Kein neues Produktionspaket, Audit- und Test-Kachel über die fünf Ingestion-Module (IMAP, POP3, SMTP, MIME, Folder-State). Zwei konkrete Lücken geschlossen: Neuer tenant_scoping_test.go in allen fünf Paketen: je zwei simulierte Mandanten mit ABSICHTLICH identischen Schlüsseln (Benutzername, Postfachname) — der Realfall, in dem ein fehlendes Scoping-Prädikat am ehesten eine echte Vermischung zeigen würde, statt trivial durch unterschiedliche Schlüssel zu bestehen. IMAP/POP3: zwei unabhängige Serverinstanzen mit je eigenem Store. SMTP: zwei Serverinstanzen, gleichzeitig mit vielen Nachrichten bedient. mimeparse: paralleles Parsen vieler "Mandanten"-Nachrichten (das Paket hat keinen Datenbankzugriff — Tenant-Scoping bedeutet hier: kein geteilter veränderlicher Zustand). folderstate: echte Postgres-Instanz, NextUID/Rebuild für Mandant A dürfen Mandant Bs Zustand nachweislich nicht verändern. mimeparse.ParseTolerant (IMP-02) war zu 0% Zeilenabdeckung vollständig ungetestet — genau der aus known-issues-archivmail.md #4 bekannte Fehler (kritische Ingestion-Logik ohne Tests). Neue tolerant_test.go: ein fehlerhafter Teil reißt die übrigen nicht mit, Gesamtgrößenlimit über alle Teile hinweg, strukturell kaputte Multipart-Hülle liefert weiterhin einen echten Fehler, Nicht-Multipart-Pfad. Abdeckung mimeparse 44,0% -> 76,7%. Testabdeckungsbericht für alle fünf Module dokumentiert, CI-Lauf auf frischem Checkout ohne externe Live-Postfächer verifiziert grün. Pflichtprüfung 3 (Stichprobenreview durch zweite Person) ist durch eine einzelne Sitzung strukturell nicht erfüllbar und bleibt offen — im Prüfprotokoll dokumentiert, Nutzer-Review ausstehend. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~29 Pakete) regressionsfrei getestet.
111 lines
4.2 KiB
Go
111 lines
4.2 KiB
Go
package mimeparse
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestParseTolerant_SingleBrokenPartDoesNotAbortWholeMessage ist die
|
|
// geforderte Pflichtprüfung/Lücke (ING-10): ParseTolerant war bislang
|
|
// vollständig ungetestet (0% Abdeckung) — genau der aus
|
|
// known-issues-archivmail.md #4 bekannte Fehler (kritische
|
|
// Ingestion-Logik ohne Tests). Ein Anhang, der die Größenbegrenzung
|
|
// überschreitet, darf die übrigen Teile NICHT mit sich reißen
|
|
// (Akzeptanzkriterium 3 des ursprünglichen Tickets IMP-02).
|
|
func TestParseTolerant_SingleBrokenPartDoesNotAbortWholeMessage(t *testing.T) {
|
|
raw := "From: a@example.com\r\n" +
|
|
"Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Guter Teil\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"zu-gross.bin\"\r\n\r\n" +
|
|
strings.Repeat("x", 1000) + "\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Zweiter guter Teil\r\n" +
|
|
"--b--\r\n"
|
|
|
|
msg, partErrors, err := ParseTolerant(strings.NewReader(raw), 100, defaultMaxSize)
|
|
if err != nil {
|
|
t.Fatalf("ParseTolerant: unerwarteter gesamtfehler: %v", err)
|
|
}
|
|
if len(partErrors) != 1 {
|
|
t.Fatalf("erwartete genau 1 teilfehler (überdimensionierter anhang), habe %d: %+v", len(partErrors), partErrors)
|
|
}
|
|
if len(msg.Parts) != 2 {
|
|
t.Fatalf("erwartete 2 verarbeitete teile trotz des fehlerhaften anhangs, habe %d", len(msg.Parts))
|
|
}
|
|
if string(msg.Parts[0].Content) != "Guter Teil" || string(msg.Parts[1].Content) != "Zweiter guter Teil" {
|
|
t.Fatalf("unerwarteter inhalt der verbleibenden teile: %+v", msg.Parts)
|
|
}
|
|
}
|
|
|
|
// TestParseTolerant_TotalSizeBudgetEnforcedAcrossParts ist
|
|
// Akzeptanzkriterium 2 des ursprünglichen Tickets IMP-02: ein
|
|
// Gesamtgrößenlimit über ALLE Teile hinweg, zusätzlich zum
|
|
// Je-Anhang-Limit.
|
|
func TestParseTolerant_TotalSizeBudgetEnforcedAcrossParts(t *testing.T) {
|
|
raw := "From: a@example.com\r\n" +
|
|
"Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"a.bin\"\r\n\r\n" +
|
|
strings.Repeat("x", 60) + "\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"b.bin\"\r\n\r\n" +
|
|
strings.Repeat("y", 60) + "\r\n" +
|
|
"--b--\r\n"
|
|
|
|
// Je-Anhang-Limit großzügig (100), Gesamtlimit knapp (80) — der
|
|
// zweite Anhang muss am GESAMTLIMIT scheitern, nicht am
|
|
// Je-Anhang-Limit.
|
|
msg, partErrors, err := ParseTolerant(strings.NewReader(raw), 100, 80)
|
|
if err != nil {
|
|
t.Fatalf("ParseTolerant: unerwarteter gesamtfehler: %v", err)
|
|
}
|
|
if len(msg.Parts) != 1 {
|
|
t.Fatalf("erwartete genau 1 teil innerhalb des gesamtbudgets, habe %d", len(msg.Parts))
|
|
}
|
|
if len(partErrors) != 1 || !errors.Is(partErrors[0].Err, ErrMessageTooLarge) {
|
|
t.Fatalf("erwartete genau 1 ErrMessageTooLarge-teilfehler, habe: %+v", partErrors)
|
|
}
|
|
}
|
|
|
|
// TestParseTolerant_StructurallyBrokenMultipartStillFails belegt: nur
|
|
// eine strukturell unlesbare Hülle (fehlende Boundary) liefert
|
|
// weiterhin einen echten Gesamtfehler — kein Teil-für-Teil-Fallback
|
|
// möglich, wie im Code dokumentiert.
|
|
func TestParseTolerant_StructurallyBrokenMultipartStillFails(t *testing.T) {
|
|
raw := "From: a@example.com\r\n" +
|
|
"Content-Type: multipart/mixed\r\n\r\n" + // keine boundary=... angegeben
|
|
"irgendwas"
|
|
|
|
_, _, err := ParseTolerant(strings.NewReader(raw), 100, defaultMaxSize)
|
|
if err == nil {
|
|
t.Fatalf("erwartete fehler bei multipart ohne boundary")
|
|
}
|
|
}
|
|
|
|
// TestParseTolerant_NonMultipartSinglePart deckt den Nicht-Multipart-
|
|
// Pfad von ParseTolerant ab (bislang ebenfalls ungetestet).
|
|
func TestParseTolerant_NonMultipartSinglePart(t *testing.T) {
|
|
raw := "From: a@example.com\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Einfache Nachricht ohne Multipart"
|
|
|
|
msg, partErrors, err := ParseTolerant(strings.NewReader(raw), defaultMaxSize, defaultMaxSize)
|
|
if err != nil {
|
|
t.Fatalf("ParseTolerant: %v", err)
|
|
}
|
|
if len(partErrors) != 0 {
|
|
t.Fatalf("unerwartete teilfehler: %+v", partErrors)
|
|
}
|
|
if len(msg.Parts) != 1 || string(msg.Parts[0].Content) != "Einfache Nachricht ohne Multipart" {
|
|
t.Fatalf("unerwartetes ergebnis: %+v", msg.Parts)
|
|
}
|
|
}
|