IMP-04: fehlerbehandlung-nicht-konformer-server
Defensive Fehlerbehandlung für nicht-RFC-konforme Mailserver beim Import, mit dokumentierten Fallback-Pfaden statt Abbruch. - client_real.go: resolveUIDValidity behandelt UIDVALIDITY=0 (bekannte archivmail-Abweichung, known-issues #5) und fehlende UIDVALIDITY-Angabe als definierten Fallback statt Sync-Abbruch — deterministisch aus dem Postfachnamen abgeleitet (FNV-1a), stabil bei wiederholten Läufen. parseFetchLines überspringt kaputte/unerwartete FETCH-Zeilen einzeln und protokolliert sie, statt den gesamten Lauf zu stoppen. Neuer Logger/WithLogger für nachvollziehbares Support-Logging. - Echten Bug behoben: die getaggte Abschlusszeile enthält ebenfalls "FETCH " und wurde zunächst fälschlich als unerwartete Antwort geloggt — jetzt nur echte Untagged-Zeilen (Präfix "* ") betrachtet. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-04-PRUEFPROTOKOLL.md): 1. TestResolveUIDValidity_ZeroTriggersDefinedFallbackNotAbort: Server meldet real UIDVALIDITY=0, Sync liefert real Fallback statt Fehler. 2. TestParseFetchLines_UnexpectedResponseSkippedRestContinue: 2 kaputte Zeilen real übersprungen+protokolliert, übrige Nachrichten kommen an. 3. TestResolveUIDValidity_RegressionGuardAgainstZeroAbort: direkter Regressionsschutz gegen den ursprünglichen UIDVALIDITY-Bug. Kein Umbau: imap/folderstate/scheduler.go unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e9947b1e28
commit
03c47d98f4
@@ -0,0 +1,164 @@
|
||||
// IMP-04: Fehlerbehandlung nicht-konformer Server. Baut einen minimalen,
|
||||
// hand-gesteuerten Fake-Server (roher TCP, KEIN mail/internal/imap) auf,
|
||||
// der bewusst nicht-konforme Antworten sendet — echte Kontrolle über
|
||||
// genau das Fehlerszenario, das getestet werden soll.
|
||||
package imapimport
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// scriptedServer nimmt EINE Verbindung an und sendet exakt die
|
||||
// vorgegebenen Zeilen als Antwort auf jedes eingehende Kommando (in
|
||||
// Reihenfolge) — genug Kontrolle, um nicht-konforme Serverantworten
|
||||
// exakt zu reproduzieren.
|
||||
type scriptedServer struct {
|
||||
responses [][]string // je eingehendem Kommando eine Antwortzeilen-Liste
|
||||
}
|
||||
|
||||
func (s *scriptedServer) start(t *testing.T) (addr string) {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listener: %v", err)
|
||||
}
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
_, _ = conn.Write([]byte("* OK IMAP4rev1 Service Ready\r\n"))
|
||||
for _, respLines := range s.responses {
|
||||
if _, err := reader.ReadString('\n'); err != nil {
|
||||
return
|
||||
}
|
||||
for _, line := range respLines {
|
||||
if _, err := conn.Write([]byte(line + "\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() { _ = listener.Close() })
|
||||
return listener.Addr().String()
|
||||
}
|
||||
|
||||
// TestResolveUIDValidity_ZeroTriggersDefinedFallbackNotAbort ist die
|
||||
// geforderte Pflichtprüfung 1: Test simuliert Server mit UIDVALIDITY=0
|
||||
// und bestätigt greifenden Fallback.
|
||||
func TestResolveUIDValidity_ZeroTriggersDefinedFallbackNotAbort(t *testing.T) {
|
||||
srv := &scriptedServer{responses: [][]string{
|
||||
{"C1 OK LOGIN completed"},
|
||||
{"* 3 EXISTS", "* OK [UIDVALIDITY 0] UIDs valid", "C2 OK [READ-WRITE] SELECT completed"},
|
||||
{"* 1 FETCH (UID 1 FLAGS ())", "* 2 FETCH (UID 2 FLAGS ())", "* 3 FETCH (UID 3 FLAGS ())", "C3 OK UID FETCH completed"},
|
||||
{"C4 OK LOGOUT completed"},
|
||||
}}
|
||||
addr := srv.start(t)
|
||||
|
||||
var loggedFallback bool
|
||||
client := NewRealClient(addr, "user", "pass").WithLogger(func(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "UIDVALIDITY=0") {
|
||||
loggedFallback = true
|
||||
}
|
||||
})
|
||||
|
||||
uidvalidity, messages, err := client.Sync(context.Background(), "INBOX")
|
||||
if err != nil {
|
||||
// Akzeptanzkriterium 1: KEIN Sync-Abbruch bei UIDVALIDITY=0.
|
||||
t.Fatalf("erwartete erfolgreichen sync trotz UIDVALIDITY=0, habe fehler: %v", err)
|
||||
}
|
||||
if uidvalidity == 0 {
|
||||
t.Fatal("erwartete definierten fallback-wert != 0, habe weiterhin 0")
|
||||
}
|
||||
if len(messages) != 3 {
|
||||
t.Fatalf("erwartete 3 nachrichten trotz UIDVALIDITY=0, habe %d", len(messages))
|
||||
}
|
||||
if !loggedFallback {
|
||||
t.Fatal("erwartete protokollierten fallback-hinweis (akzeptanzkriterium 3: nachvollziehbar)")
|
||||
}
|
||||
|
||||
// Fallback ist deterministisch für dasselbe Postfach — ein zweiter
|
||||
// Aufruf gegen einen erneut nicht-konformen Server liefert real
|
||||
// denselben Ersatzwert, löst also keinen unnötigen Voll-Resync bei
|
||||
// jedem einzelnen Lauf aus.
|
||||
if fallbackUIDValidity("INBOX") != uidvalidity {
|
||||
t.Fatalf("erwartete deterministischen fallback, habe %d vs %d", fallbackUIDValidity("INBOX"), uidvalidity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseFetchLines_UnexpectedResponseSkippedRestContinue ist die
|
||||
// geforderte Pflichtprüfung 2: Test mit unerwarteter/kaputter
|
||||
// Serverantwort bestätigt Weiterlauf für übrige Nachrichten.
|
||||
func TestParseFetchLines_UnexpectedResponseSkippedRestContinue(t *testing.T) {
|
||||
srv := &scriptedServer{responses: [][]string{
|
||||
{"C1 OK LOGIN completed"},
|
||||
{"* 3 EXISTS", "* OK [UIDVALIDITY 42] UIDs valid", "C2 OK [READ-WRITE] SELECT completed"},
|
||||
{
|
||||
"* 1 FETCH (UID 1 FLAGS ())",
|
||||
"* GARBAGE NOT EVEN A FETCH LINE AT ALL", // kaputte/unerwartete Antwort
|
||||
"* 2 FETCH SOMETHING UNPARSEABLE HERE (UID)", // ebenfalls kaputt
|
||||
"* 3 FETCH (UID 3 FLAGS (\\Seen))",
|
||||
"C3 OK UID FETCH completed",
|
||||
},
|
||||
{"C4 OK LOGOUT completed"},
|
||||
}}
|
||||
addr := srv.start(t)
|
||||
|
||||
var skippedCount int
|
||||
client := NewRealClient(addr, "user", "pass").WithLogger(func(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "unerwartete server-antwort übersprungen") {
|
||||
skippedCount++
|
||||
}
|
||||
})
|
||||
|
||||
uidvalidity, messages, err := client.Sync(context.Background(), "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("erwartete erfolgreichen sync trotz kaputter zeilen, habe fehler: %v", err)
|
||||
}
|
||||
if uidvalidity != 42 {
|
||||
t.Fatalf("erwartete uidvalidity=42, habe %d", uidvalidity)
|
||||
}
|
||||
// Akzeptanzkriterium 2: die BEIDEN kaputten Zeilen werden übersprungen
|
||||
// UND protokolliert, die ÜBRIGEN (real 2) Nachrichten kommen trotzdem an.
|
||||
if len(messages) != 2 {
|
||||
t.Fatalf("erwartete 2 lesbare nachrichten trotz kaputter zeilen, habe %d: %+v", len(messages), messages)
|
||||
}
|
||||
if skippedCount != 2 {
|
||||
t.Fatalf("erwartete 2 protokollierte übersprungene zeilen, habe %d", skippedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveUIDValidity_RegressionGuardAgainstZeroAbort ist die
|
||||
// geforderte Pflichtprüfung 3: Regressionstest verhindert
|
||||
// Wiederauftreten des UIDVALIDITY-Bugs — prüft die Fallback-Funktion
|
||||
// isoliert und direkt, unabhängig vom Netzwerkpfad.
|
||||
func TestResolveUIDValidity_RegressionGuardAgainstZeroAbort(t *testing.T) {
|
||||
client := NewRealClient("unused:0", "u", "p")
|
||||
value, err := client.resolveUIDValidity([]string{"* OK [UIDVALIDITY 0] UIDs valid"}, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("regression: UIDVALIDITY=0 löste real einen fehler aus (der genau vermiedene bug): %v", err)
|
||||
}
|
||||
if value == 0 {
|
||||
t.Fatal("regression: fallback lieferte weiterhin 0 — bug erneut aufgetreten")
|
||||
}
|
||||
|
||||
// Fehlende UIDVALIDITY-Angabe (noch nicht-konformer als 0) darf
|
||||
// ebenfalls nicht abbrechen.
|
||||
value2, err := client.resolveUIDValidity([]string{"C2 OK SELECT completed"}, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("regression: fehlende UIDVALIDITY löste real einen fehler aus: %v", err)
|
||||
}
|
||||
if value2 == 0 {
|
||||
t.Fatal("regression: fallback bei fehlender UIDVALIDITY lieferte 0")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user