// Integrationstests (INT-06): echte Postgres-Instanz über // TEST_TENANT_DSN, echter mailrules.Store (kein Fake) — dieselbe // Konvention wie mailrules/engine_test.go. package mailrulesapi import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "strconv" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/mail/internal/mailrules" ) func setupRealStore(t *testing.T) *mailrules.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(pool.Close) store := mailrules.NewStore(pool) if err := store.EnsureSchema(ctx); err != nil { t.Fatalf("schema: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM mail_rules WHERE tenant_slug LIKE 'mandant-int06-%'`) }) return store } func setupRealTestServer(t *testing.T) (*httptest.Server, *mailrules.Store) { t.Helper() store := setupRealStore(t) srv := NewServer(store) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) return ts, store } // TestIntegration_RuleSetViaAPIAppliedCorrectlyByEngine ist die // geforderte Pflichtprüfung 2 (INT-06): eine über die API gesetzte // Regel wird beim nächsten Import korrekt angewendet — real geprüft, // indem die über einen echten HTTP-Request angelegte Regel über // denselben Store gelesen und in mailrules.Engine (IMP-03, // unverändert) ausgewertet wird. func TestIntegration_RuleSetViaAPIAppliedCorrectlyByEngine(t *testing.T) { ts, store := setupRealTestServer(t) tenant := "mandant-int06-anwendung" body, _ := json.Marshal(ruleDTO{ Name: "Rechnungsregel", SenderPattern: `rechnung@lieferant\.example`, Category: "Rechnung", Tag: "finanzen", Priority: 5, }) resp, err := http.Post(ts.URL+"/api/v1/mail/rules?tenant="+tenant, "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("regel über api anlegen: %v", err) } if resp.StatusCode != http.StatusCreated { t.Fatalf("erwartete 201, habe %d", resp.StatusCode) } _ = resp.Body.Close() // Genau der Weg, den IMP-03s Import-Pfad geht: Store.List -> // mailrules.NewEngine -> Evaluate. Kein separater API-Auswertungspfad. rules, err := store.List(context.Background(), tenant) if err != nil { t.Fatalf("regeln lesen: %v", err) } engine, err := mailrules.NewEngine(rules) if err != nil { t.Fatalf("engine bauen: %v", err) } result := engine.Evaluate(mailrules.EmailMetadata{Sender: "rechnung@lieferant.example"}) if result.Category != "Rechnung" { t.Fatalf("erwartete kategorie \"Rechnung\" für die über die API gesetzte regel, habe: %+v", result) } } // TestIntegration_RuleChangeIsolatedPerTenant ist die geforderte // Pflichtprüfung 3 (INT-06): eine Regeländerung eines Mandanten wirkt // nicht auf andere Mandanten — real über die API und den echten Store // geprüft, inklusive Update/Delete-Versuch mit einer ECHTEN, bekannten // fremden ID. func TestIntegration_RuleChangeIsolatedPerTenant(t *testing.T) { ts, store := setupRealTestServer(t) tenantA := "mandant-int06-isoliert-a" tenantB := "mandant-int06-isoliert-b" body, _ := json.Marshal(ruleDTO{Name: "Nur A", SenderPattern: "^a@", Category: "A", Priority: 1}) resp, err := http.Post(ts.URL+"/api/v1/mail/rules?tenant="+tenantA, "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("regel für mandant a anlegen: %v", err) } var created ruleDTO _ = json.NewDecoder(resp.Body).Decode(&created) _ = resp.Body.Close() // Mandant B sieht die Regel von Mandant A nicht. rulesB, err := store.List(context.Background(), tenantB) if err != nil { t.Fatalf("list mandant b: %v", err) } if len(rulesB) != 0 { t.Fatalf("mandant b sieht regeln von mandant a: %+v", rulesB) } // Mandant B versucht mit der ECHTEN, bekannten ID von Mandant A zu // aktualisieren — muss scheitern. updateBody, _ := json.Marshal(ruleDTO{Name: "Übernommen von B", Category: "B", Priority: 999}) req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/mail/rules/"+strconv.FormatInt(created.ID, 10)+"?tenant="+tenantB, bytes.NewReader(updateBody)) updResp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("update-versuch mandant b: %v", err) } if updResp.StatusCode != http.StatusNotFound { t.Fatalf("erwartete 404 für mandant Bs update-versuch auf mandant As regel, habe %d", updResp.StatusCode) } _ = updResp.Body.Close() // Mandant As Regel ist unverändert. rulesA, err := store.List(context.Background(), tenantA) if err != nil { t.Fatalf("list mandant a: %v", err) } if len(rulesA) != 1 || rulesA[0].Category != "A" || rulesA[0].Name != "Nur A" { t.Fatalf("mandant As regel wurde durch mandant Bs versuch verändert: %+v", rulesA) } }