package mailrulesapi import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "sync" "testing" "gitea.perlbach24.de/scripte/nexarch/mail/internal/mailrules" ) // fakeRulesStore ist ein In-Memory-Fake für RulesStore — reine // Vertragstests brauchen keine echte Postgres-Instanz. type fakeRulesStore struct { mu sync.Mutex nextID int64 byTenant map[string]map[int64]mailrules.Rule } func newFakeRulesStore() *fakeRulesStore { return &fakeRulesStore{byTenant: map[string]map[int64]mailrules.Rule{}} } func (f *fakeRulesStore) Create(_ context.Context, tenantSlug string, rule mailrules.Rule) (int64, error) { f.mu.Lock() defer f.mu.Unlock() f.nextID++ rule.ID = f.nextID if f.byTenant[tenantSlug] == nil { f.byTenant[tenantSlug] = map[int64]mailrules.Rule{} } f.byTenant[tenantSlug][rule.ID] = rule return rule.ID, nil } func (f *fakeRulesStore) List(_ context.Context, tenantSlug string) ([]mailrules.Rule, error) { f.mu.Lock() defer f.mu.Unlock() var rules []mailrules.Rule for _, r := range f.byTenant[tenantSlug] { rules = append(rules, r) } return rules, nil } func (f *fakeRulesStore) Update(_ context.Context, tenantSlug string, id int64, rule mailrules.Rule) error { f.mu.Lock() defer f.mu.Unlock() if _, ok := f.byTenant[tenantSlug][id]; !ok { return mailrules.ErrNotFound } rule.ID = id f.byTenant[tenantSlug][id] = rule return nil } func (f *fakeRulesStore) Delete(_ context.Context, tenantSlug string, id int64) error { f.mu.Lock() defer f.mu.Unlock() delete(f.byTenant[tenantSlug], id) return nil } func setupTestServer(t *testing.T) (*httptest.Server, *fakeRulesStore) { t.Helper() store := newFakeRulesStore() srv := NewServer(store) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) return ts, store } // TestContract_CreateUpdateDeletePrioritize ist die geforderte // Pflichtprüfung 1 (INT-06): Vertragstest deckt Anlegen/Ändern/Löschen/ // Priorisieren ab. func TestContract_CreateUpdateDeletePrioritize(t *testing.T) { ts, _ := setupTestServer(t) // Anlegen. createBody, _ := json.Marshal(ruleDTO{Name: "Rechnungen", SenderPattern: "^rechnung@", Category: "Finanzen", Priority: 10}) resp, err := http.Post(ts.URL+"/api/v1/mail/rules?tenant=mandant-a", "application/json", bytes.NewReader(createBody)) if err != nil { t.Fatalf("create: %v", err) } if resp.StatusCode != http.StatusCreated { t.Fatalf("erwartete 201, habe %d", resp.StatusCode) } var created ruleDTO _ = json.NewDecoder(resp.Body).Decode(&created) _ = resp.Body.Close() if created.ID == 0 { t.Fatalf("erwartete gesetzte id nach anlage") } // Priorisieren/Ändern. updateBody, _ := json.Marshal(ruleDTO{Name: "Rechnungen (neu)", SenderPattern: "^rechnung@", Category: "Finanzen", Priority: 1}) req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/mail/rules/"+strconv.FormatInt(created.ID, 10)+"?tenant=mandant-a", bytes.NewReader(updateBody)) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("update: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("erwartete 200, habe %d", resp.StatusCode) } var updated ruleDTO _ = json.NewDecoder(resp.Body).Decode(&updated) _ = resp.Body.Close() if updated.Priority != 1 || updated.Name != "Rechnungen (neu)" { t.Fatalf("update nicht angekommen: %+v", updated) } // Einsehen (Liste). resp, err = http.Get(ts.URL + "/api/v1/mail/rules?tenant=mandant-a") if err != nil { t.Fatalf("list: %v", err) } var list listRulesResponse _ = json.NewDecoder(resp.Body).Decode(&list) _ = resp.Body.Close() if len(list.Rules) != 1 || list.Rules[0].Priority != 1 { t.Fatalf("liste zeigt nicht die aktualisierte regel: %+v", list.Rules) } // Löschen. req, _ = http.NewRequest(http.MethodDelete, ts.URL+"/api/v1/mail/rules/"+strconv.FormatInt(created.ID, 10)+"?tenant=mandant-a", nil) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("delete: %v", err) } if resp.StatusCode != http.StatusNoContent { t.Fatalf("erwartete 204, habe %d", resp.StatusCode) } _ = resp.Body.Close() resp, err = http.Get(ts.URL + "/api/v1/mail/rules?tenant=mandant-a") if err != nil { t.Fatalf("list nach löschen: %v", err) } var listAfter listRulesResponse _ = json.NewDecoder(resp.Body).Decode(&listAfter) _ = resp.Body.Close() if len(listAfter.Rules) != 0 { t.Fatalf("erwartete leere liste nach löschen, habe %+v", listAfter.Rules) } } // TestListCreate_RejectMissingTenant deckt die geforderte // tenant-Pflicht ab (dieselbe Konvention wie mailapi/INT-01). func TestListCreate_RejectMissingTenant(t *testing.T) { ts, _ := setupTestServer(t) resp, err := http.Get(ts.URL + "/api/v1/mail/rules") if err != nil { t.Fatalf("get: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("erwartete 400 ohne tenant, habe %d", resp.StatusCode) } }