package moduleadapter import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) // fakeVerifier simuliert Core TEN-01 fuer die Handler-Tests, ohne einen // echten Core-Dienst zu benoetigen (Muster wie // archive/internal/moduleadapter, RET-12). type fakeVerifier struct { validTenants map[string]bool err error } func (f fakeVerifier) VerifyTenant(_ context.Context, tenantSlug string) (bool, error) { if f.err != nil { return false, f.err } return f.validTenants[tenantSlug], nil } func newTestServer(t *testing.T) (*Server, string) { t.Helper() pool := testPool(t) token := "test-service-token-secret" verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}} return NewServer(pool, token, "mandant-a", verifier), token } func TestHandleRegister_RequiresServiceToken(t *testing.T) { srv, token := newTestServer(t) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"}) // Ohne Token: 401. resp, err := http.Post(ts.URL+"/register", "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("anfrage ohne token: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("erwarte 401 ohne token, habe %d", resp.StatusCode) } // Falsches Token: 401. req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", "falsch") req.Header.Set("Content-Type", "application/json") resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("anfrage mit falschem token: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("erwarte 401 mit falschem token, habe %d", resp.StatusCode) } // Richtiges Token: 200. req, _ = http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("anfrage mit richtigem token: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("erwarte 200 mit richtigem token, habe %d", resp.StatusCode) } } func TestHandleRegisterAndList_EndToEnd(t *testing.T) { srv, token := newTestServer(t) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "mail-fiktiv", ObjectType: "message", Triggers: []string{"mail.received"}, TenantSlug: "mandant-a"}) req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("registrieren: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("erwarte 200, habe %d", resp.StatusCode) } req, _ = http.NewRequest(http.MethodGet, ts.URL+"/registrations", nil) req.Header.Set("X-Service-Token", token) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("auflisten: %v", err) } defer resp.Body.Close() var regs []Registration if err := json.NewDecoder(resp.Body).Decode(®s); err != nil { t.Fatalf("antwort dekodieren: %v", err) } if len(regs) != 1 || regs[0].ModuleName != "mail-fiktiv" { t.Fatalf("erwarte 1 registrierung fuer mail-fiktiv, habe %+v", regs) } } // ENG-06 Pruefung 1: Registrierung mit gueltigem, existierendem // Tenant-Kontext wird uebernommen. func TestHandleRegister_ValidTenantContextAccepted(t *testing.T) { srv, token := newTestServer(t) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"}) req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("registrieren: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("erwarte 200 fuer gueltigen tenant, habe %d", resp.StatusCode) } } // ENG-06 Pruefung 2: Registrierung mit abweichendem Tenant-Kontext // (passt nicht zur Server-Instanz) wird abgelehnt und nicht uebernommen. func TestHandleRegister_ForeignTenantSlugRejected(t *testing.T) { srv, token := newTestServer(t) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-fremd"}) req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("registrieren: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("erwarte 403 fuer fremden tenant, habe %d", resp.StatusCode) } if _, err := TriggersFor(context.Background(), srv.pool, "dms-fiktiv", "invoice"); err == nil { t.Fatal("erwarte keine uebernommene registrierung fuer fremden tenant") } } // ENG-06 Pruefung 2 (Variante): die Server-Instanz ist zwar fuer // "mandant-a" konfiguriert, aber die Core-Registry kennt diesen Tenant // nicht (mehr) -- die Registrierung darf sich nicht allein auf den // lokal konfigurierten Tenant-Slug verlassen, sondern muss unabhaengig // gegen Core pruefen. func TestHandleRegister_TenantUnknownToCoreRegistryRejected(t *testing.T) { pool := testPool(t) token := "test-service-token-secret" verifier := fakeVerifier{validTenants: map[string]bool{}} srv := NewServer(pool, token, "mandant-a", verifier) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"}) req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("registrieren: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("erwarte 403 wenn core-registry tenant nicht bestaetigt, habe %d", resp.StatusCode) } if _, err := TriggersFor(context.Background(), pool, "dms-fiktiv", "invoice"); err == nil { t.Fatal("erwarte keine uebernommene registrierung ohne core-bestaetigung") } } // ENG-06 Regressionstest: Core-Registry-Aufruf schlaegt technisch fehl // (Transportfehler) -> fail closed, keine Registrierung. func TestHandleRegister_TenantRegistryCallFailsRejected(t *testing.T) { pool := testPool(t) token := "test-service-token-secret" verifier := fakeVerifier{err: context.DeadlineExceeded} srv := NewServer(pool, token, "mandant-a", verifier) mux := http.NewServeMux() srv.RegisterRoutes(mux) ts := httptest.NewServer(mux) defer ts.Close() body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"}) req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body)) req.Header.Set("X-Service-Token", token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("registrieren: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("erwarte 403 bei fehlgeschlagenem core-aufruf (fail closed), habe %d", resp.StatusCode) } }