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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
225 lines
7.9 KiB
Go
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(®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)
|
|
}
|
|
}
|