feat(mail): ING-10 Ingestion-Testsuite — Tenant-Scoping-Tests, mimeparse-Lücke geschlossen
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.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package folderstate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestTenantScoping_NeverReturnsOrMutatesOtherTenantsFolderState ist die
|
||||
// geforderte Pflichtprüfung (ING-10, Akzeptanzkriterium 2): Tenant-
|
||||
// Scoping für den Folder-State-Ingestion-Pfad. Zwei Mandanten mit
|
||||
// IDENTISCHEM Postfachnamen "INBOX" — der Realfall, in dem ein fehlendes
|
||||
// tenant_slug-Prädikat sofort eine Vermischung zeigen würde.
|
||||
func TestTenantScoping_NeverReturnsOrMutatesOtherTenantsFolderState(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
tenantA := "mandant-ing10-scoping-a"
|
||||
tenantB := "mandant-ing10-scoping-b"
|
||||
t.Cleanup(func() {
|
||||
_, _ = store.pool.Exec(context.Background(), `DELETE FROM mail_folder_state WHERE tenant_slug LIKE 'mandant-ing10-%'`)
|
||||
_, _ = store.pool.Exec(context.Background(), `DELETE FROM mail_folder_state_events WHERE tenant_slug LIKE 'mandant-ing10-%'`)
|
||||
})
|
||||
|
||||
stateA, err := store.GetOrCreate(ctx, tenantA, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate mandant a: %v", err)
|
||||
}
|
||||
stateB, err := store.GetOrCreate(ctx, tenantB, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate mandant b: %v", err)
|
||||
}
|
||||
if stateA.UIDValidity == stateB.UIDValidity {
|
||||
// Extrem unwahrscheinlich (beide UIDVALIDITY sind
|
||||
// Unix-Zeitstempel), aber falls doch: kein Blocker für den
|
||||
// eigentlichen Isolationstest, nur ein Hinweis für den Leser.
|
||||
t.Logf("hinweis: beide mandanten haben zufällig dieselbe uidvalidity bekommen (%d)", stateA.UIDValidity)
|
||||
}
|
||||
|
||||
// UIDs für Mandant A vergeben — dürfen Mandant Bs Zustand NICHT
|
||||
// verändern.
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := store.NextUID(ctx, tenantA, "INBOX"); err != nil {
|
||||
t.Fatalf("NextUID mandant a: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
afterA, err := store.CurrentState(ctx, tenantA, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentState mandant a: %v", err)
|
||||
}
|
||||
stillB, err := store.CurrentState(ctx, tenantB, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentState mandant b: %v", err)
|
||||
}
|
||||
|
||||
if afterA.UIDNext != stateA.UIDNext+5 {
|
||||
t.Fatalf("mandant a: erwartete UIDNext %d, habe %d", stateA.UIDNext+5, afterA.UIDNext)
|
||||
}
|
||||
if stillB.UIDNext != stateB.UIDNext {
|
||||
t.Fatalf("mandantenvermischung: mandant b's UIDNext hat sich durch mandant a's NextUID-Aufrufe verändert (%d -> %d)", stateB.UIDNext, stillB.UIDNext)
|
||||
}
|
||||
|
||||
// Rebuild für Mandant B darf Mandant As Zustand nicht berühren.
|
||||
rebuiltB, err := store.Rebuild(ctx, tenantB, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("Rebuild mandant b: %v", err)
|
||||
}
|
||||
if rebuiltB.UIDValidity == stateB.UIDValidity {
|
||||
t.Fatalf("Rebuild mandant b hat UIDVALIDITY nicht geändert")
|
||||
}
|
||||
unchangedA, err := store.CurrentState(ctx, tenantA, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentState mandant a nach Rebuild b: %v", err)
|
||||
}
|
||||
if unchangedA.UIDValidity != afterA.UIDValidity {
|
||||
t.Fatalf("mandantenvermischung: mandant a's UIDVALIDITY hat sich durch mandant b's Rebuild verändert")
|
||||
}
|
||||
|
||||
// Events sind ebenfalls strikt je Mandant getrennt.
|
||||
eventsA, err := store.Events(ctx, tenantA, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatalf("Events mandant a: %v", err)
|
||||
}
|
||||
for _, e := range eventsA {
|
||||
if e.EventType == EventRebuilt {
|
||||
t.Fatalf("mandant a hat mandant b's Rebuild-Event gesehen: %+v", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user