Files
rootandClaude Sonnet 5 f5697871e1
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
ENG-06: unabhaengige-tenant-validierung-bei-objekttyp-registrierung-durch-dms-mail
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 19:09:05 +00:00

225 lines
7.9 KiB
Go

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(&regs); 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)
}
}