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.
90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|