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) } } }