// Integrationstest (SRC-08): echte Postgres- UND Manticore-Instanz, // folgt derselben TEST_*-Env-Konvention wie mail/internal/search. package savedsearch import ( "context" "os" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/mail/internal/search" ) func setupStore(t *testing.T) *Store { t.Helper() dsn := os.Getenv("TEST_TENANT_DSN") if dsn == "" { t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("pool: %v", err) } t.Cleanup(func() { pool.Close() }) store := NewStore(pool) if err := store.EnsureSchema(ctx); err != nil { t.Fatalf("schema: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM mail_saved_searches WHERE tenant_slug LIKE 'mandant-src08-%'`) }) return store } func setupSearchClient(t *testing.T) *search.Client { t.Helper() baseURL := os.Getenv("TEST_MANTICORE_URL") if baseURL == "" { t.Skip("TEST_MANTICORE_URL nicht gesetzt, Integrationstest übersprungen") } client := search.NewClient(baseURL) if err := client.EnsureSchema(context.Background()); err != nil { t.Fatalf("search-schema: %v", err) } return client } // TestExecute_SavedSearchWithMultipleFiltersReproducesCorrectly ist die // geforderte Pflichtprüfung 1: gespeicherte Suche mit mehreren Filtern // wird korrekt reproduziert. func TestExecute_SavedSearchWithMultipleFiltersReproducesCorrectly(t *testing.T) { store := setupStore(t) searchClient := setupSearchClient(t) ctx := context.Background() tenant := "mandant-src08-filter" docs := []search.Document{ {MessageID: "msg-sv-treffer", Subject: "Quartalsbericht", Sender: "alice@example.com", Mailbox: "inbox"}, {MessageID: "msg-sv-falscher-sender", Subject: "Quartalsbericht", Sender: "bob@example.com", Mailbox: "inbox"}, {MessageID: "msg-sv-falsche-mailbox", Subject: "Quartalsbericht", Sender: "alice@example.com", Mailbox: "archiv"}, } for _, d := range docs { d.TenantSlug = tenant d.ID = search.DocumentID(tenant, d.MessageID) if err := searchClient.Index(ctx, d); err != nil { t.Fatalf("index %s: %v", d.MessageID, err) } } filters := []search.FacetFilter{ {Field: search.FieldSender, Value: "alice@example.com"}, {Field: search.FieldMailbox, Value: "inbox"}, } id, err := store.Save(ctx, tenant, "user-1", "Alice Inbox Quartalsbericht", "Quartalsbericht", filters) if err != nil { t.Fatalf("save: %v", err) } saved, err := store.Get(ctx, tenant, "user-1", id) if err != nil { t.Fatalf("get: %v", err) } results, err := Execute(ctx, searchClient, saved) if err != nil { t.Fatalf("execute: %v", err) } if len(results) != 1 || results[0].MessageID != "msg-sv-treffer" { t.Fatalf("erwartete genau msg-sv-treffer, habe: %+v", results) } } // TestExecute_ReturnsCurrentResultsNotFrozenSnapshot deckt // Akzeptanzkriterium 2 ab: aktuelle Treffer, kein eingefrorener Snapshot. func TestExecute_ReturnsCurrentResultsNotFrozenSnapshot(t *testing.T) { store := setupStore(t) searchClient := setupSearchClient(t) ctx := context.Background() tenant := "mandant-src08-live" docID := search.DocumentID(tenant, "msg-live-1") // Verteidigung gegen Testreste eines vorherigen Laufs (Manticore wird // zwischen Testläufen nicht automatisch geleert, anders als Postgres // über t.Cleanup) — Ausgangszustand muss real leer sein, damit der // Test die Aussage "kein eingefrorener Snapshot" beweiskräftig prüft. _ = searchClient.Delete(ctx, docID) t.Cleanup(func() { _ = searchClient.Delete(context.Background(), docID) }) id, err := store.Save(ctx, tenant, "user-1", "Alles zu Zylotharion", "Zylotharion", nil) if err != nil { t.Fatalf("save: %v", err) } saved, err := store.Get(ctx, tenant, "user-1", id) if err != nil { t.Fatalf("get: %v", err) } before, err := Execute(ctx, searchClient, saved) if err != nil { t.Fatalf("execute (vorher): %v", err) } if len(before) != 0 { t.Fatalf("erwartete 0 treffer vor indexierung, habe %d", len(before)) } if err := searchClient.Index(ctx, search.Document{ ID: docID, TenantSlug: tenant, MessageID: "msg-live-1", Subject: "Neuigkeiten zu Zylotharion", }); err != nil { t.Fatalf("index: %v", err) } after, err := Execute(ctx, searchClient, saved) if err != nil { t.Fatalf("execute (nachher): %v", err) } if len(after) != 1 { t.Fatalf("erwartete 1 aktuellen treffer nach neuer indexierung (kein eingefrorener snapshot), habe %d", len(after)) } } // TestList_UserSeesNoOtherTenantsSavedSearches ist die geforderte // Pflichtprüfung 2: Benutzer sieht keine gespeicherten Suchen anderer // Mandanten. func TestList_UserSeesNoOtherTenantsSavedSearches(t *testing.T) { store := setupStore(t) ctx := context.Background() tenantA := "mandant-src08-iso-a" tenantB := "mandant-src08-iso-b" if _, err := store.Save(ctx, tenantA, "user-1", "Suche A", "irgendwas", nil); err != nil { t.Fatalf("save mandant a: %v", err) } if _, err := store.Save(ctx, tenantB, "user-1", "Suche B", "irgendwas", nil); err != nil { t.Fatalf("save mandant b: %v", err) } listB, err := store.List(ctx, tenantB, "user-1") if err != nil { t.Fatalf("list mandant b: %v", err) } if len(listB) != 1 || listB[0].Name != "Suche B" { t.Fatalf("mandant b sieht fremde/fehlende suchen: %+v", listB) } } // TestDelete_RemovesOnlyThatSavedSearch ist die geforderte Pflichtprüfung // 3: Löschen einer gespeicherten Suche entfernt nur diese. func TestDelete_RemovesOnlyThatSavedSearch(t *testing.T) { store := setupStore(t) ctx := context.Background() tenant := "mandant-src08-delete" idA, err := store.Save(ctx, tenant, "user-1", "Suche A", "a", nil) if err != nil { t.Fatalf("save a: %v", err) } idB, err := store.Save(ctx, tenant, "user-1", "Suche B", "b", nil) if err != nil { t.Fatalf("save b: %v", err) } if err := store.Delete(ctx, tenant, "user-1", idA); err != nil { t.Fatalf("delete a: %v", err) } if _, err := store.Get(ctx, tenant, "user-1", idA); err != ErrNotFound { t.Fatalf("erwartete ErrNotFound für gelöschte suche a, habe: %v", err) } stillThere, err := store.Get(ctx, tenant, "user-1", idB) if err != nil { t.Fatalf("suche b sollte weiterhin existieren: %v", err) } if stillThere.Name != "Suche B" { t.Fatalf("unerwarteter inhalt für suche b: %+v", stillThere) } }