Merge PR #325: ENG-06: unabhaengige-tenant-validierung-bei-objekttyp-registrierung-durch-dms-mail
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) 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 / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s

This commit is contained in:
patrick committed 2026-10-19 02:20:51 +02:00
commit 0dd05dec88
8 files changed
+317 -9

No files matched your search

+1
View File
@@ -106,6 +106,7 @@ func workflowEventConfig() workflowevent.Config {
return workflowevent.Config{
BaseURL: os.Getenv("NEXARCH_DMS_APP_WORKFLOW_URL"),
ServiceToken: os.Getenv("NEXARCH_DMS_APP_WORKFLOW_SERVICE_TOKEN"),
TenantSlug: os.Getenv("NEXARCH_DMS_APP_TENANT_SLUG"),
}
}
+1
View File
@@ -252,6 +252,7 @@ func startJobWorkers(ctx context.Context, pool *pgxpool.Pool, queue *jobqueue.Qu
deps.WorkflowEvent = workflowevent.Config{
BaseURL: os.Getenv("NEXARCH_DMS_WORKER_WORKFLOW_URL"),
ServiceToken: os.Getenv("NEXARCH_DMS_WORKER_WORKFLOW_SERVICE_TOKEN"),
TenantSlug: tenantSlug,
}
if deps.WorkflowEvent.Enabled() {
// INT-13 Akzeptanzkriterium 1: Objekttyp + Trigger beim Start
+7 -2
View File
@@ -33,10 +33,14 @@ const (
var Triggers = []string{TriggerCreated, TriggerStatusChanged, TriggerDeadlineReached}
// Config steuert Ziel und Zugangsdaten. BaseURL leer bedeutet
// abgeschaltet (Standard aus, analog zu FDN-18).
// abgeschaltet (Standard aus, analog zu FDN-18). TenantSlug ist der
// Tenant-Kontext dieser DMS-Instanz (ENG-06: Workflow prueft ihn
// unabhaengig gegen die Core-Tenant-Registry, bevor eine Registrierung
// uebernommen wird).
type Config struct {
BaseURL string
ServiceToken string
TenantSlug string
}
// Enabled meldet, ob eine Workflow-Anbindung konfiguriert ist.
@@ -46,6 +50,7 @@ type registerRequest struct {
ModuleName string `json:"module_name"`
ObjectType string `json:"object_type"`
Triggers []string `json:"triggers"`
TenantSlug string `json:"tenant_slug"`
}
// Register registriert den Objekttyp "document" mit seinen Auslöser-
@@ -55,7 +60,7 @@ func Register(ctx context.Context, cfg Config) error {
if !cfg.Enabled() {
return nil
}
body, err := json.Marshal(registerRequest{ModuleName: ModuleName, ObjectType: ObjectType, Triggers: Triggers})
body, err := json.Marshal(registerRequest{ModuleName: ModuleName, ObjectType: ObjectType, Triggers: Triggers, TenantSlug: cfg.TenantSlug})
if err != nil {
return fmt.Errorf("workflowevent: registrierung serialisieren: %w", err)
}
@@ -91,7 +91,7 @@ func TestRegister_SendsObjectTypeAndTriggers(t *testing.T) {
srv := fw.server()
defer srv.Close()
cfg := Config{BaseURL: srv.URL, ServiceToken: "secret-token"}
cfg := Config{BaseURL: srv.URL, ServiceToken: "secret-token", TenantSlug: "mandant-a"}
if err := Register(context.Background(), cfg); err != nil {
t.Fatalf("register: %v", err)
}
@@ -105,6 +105,11 @@ func TestRegister_SendsObjectTypeAndTriggers(t *testing.T) {
if len(got.Triggers) != len(Triggers) {
t.Fatalf("triggers = %v, want %v", got.Triggers, Triggers)
}
// ENG-06: Workflow prueft tenant_slug unabhaengig gegen Core, DMS
// muss ihn also mitschicken, sonst wird die Registrierung abgelehnt.
if got.TenantSlug != "mandant-a" {
t.Fatalf("tenant_slug = %q, want mandant-a", got.TenantSlug)
}
}
// TestRegister_Disabled prueft: ohne BaseURL passiert nichts (Standard aus).
+1 -1
View File
@@ -121,7 +121,7 @@ func buildMux(pool *pgxpool.Pool, taskStore *task.Store, serviceToken, tenantSlu
caseoverviewapi.NewServer(caseoverviewSvc).RegisterRoutes(mux)
backchannelapi.NewServer(backchannelSvc).RegisterRoutes(mux)
routingruleapi.NewServer(routingruleStore, buildRoutingSuggester()).RegisterRoutes(mux)
moduleadapter.NewServer(pool, serviceToken, tenantSlug).RegisterRoutes(mux)
moduleadapter.NewServer(pool, serviceToken, tenantSlug, tenantVerifier).RegisterRoutes(mux)
return mux, nil
}
+132
View File
@@ -0,0 +1,132 @@
# ENG-06 – Pruefprotokoll: Unabhaengige Tenant-Validierung bei Objekttyp-Registrierung durch DMS/Mail
Durchgefuehrt am 2026-10-16 gegen echten Postgres auf Testhost
192.168.1.204 (eigene Wegwerf-Schemas ueber `internal/testdb`, kein
Bezug zu archivdms-Daten).
## Ausgangslage
ENG-02 (`internal/moduleadapter`, `Register`/`handleRegister`) nahm
Objekttyp- und Trigger-Registrierungen von DMS/Mail bislang ohne jeden
Tenant-Kontext entgegen — `registerRequest` kannte kein `tenant_slug`.
Das ist die gleiche Luecke, die RET-12 fuer Archive bereits geschlossen
hat (`archive/internal/moduleadapter`: `TenantVerifier`-Interface +
Pruefung in `RegisterHandler` gegen Core TEN-01, bevor `Register`
aufgerufen wird) und die VOR-06 fuer Workflows Rueckkanal
(`internal/backchannel`) mit demselben Muster (Abgleich gegen
`ownTenantSlug` UND gegen die Core-Registry) bereits umgesetzt hat.
ENG-09 hatte fuer `/events` (`PublishEvent`) zwar schon einen
`tenant_slug`/`ownTenantSlug`-Abgleich, aber keinen Core-Registry-Aufruf
und keine Pruefung bei der eigentlichen Registrierung — dieses Ticket
schliesst die Luecke fuer `/register`.
## Umsetzung
- `workflow/internal/moduleadapter/handler.go`: neues
`TenantVerifier`-Interface (gleiches Muster wie Archive RET-12);
`Server` erhaelt ein `tenants TenantVerifier`-Feld, `NewServer` nimmt
es als vierten Parameter entgegen. Neue Methode `tenantValid` prueft
zweifach, bevor eine Registrierung uebernommen wird: (1) der
mitgelieferte `tenant_slug` muss zur Server-Instanz passen
(`ownTenantSlug`, Modell A — ein Prozess/eine DB je Mandant), (2)
unabhaengig davon bestaetigt `tenants.VerifyTenant` gegen Core TEN-01,
dass der Tenant dort tatsaechlich gefuehrt wird. Ein fehlgeschlagener
Core-Aufruf zaehlt als ungueltig (fail closed). `registerRequest` hat
jetzt ein Pflichtfeld `tenant_slug`; `handleRegister` lehnt bei
ungueltigem Tenant-Kontext mit 403 ab und protokolliert
Modul/Objekttyp/Tenant. `Register`/`Rollback`/`Deregister` (DB-Ebene)
bleiben unveraendert — die Pruefung sitzt an der API-Grenze, analog zu
`backchannel.ForObject` (VOR-06), keine Aenderung an Schema oder
Fachlogik noetig (kleinste Loesung).
- `workflow/cmd/workflow-app/main.go`: `moduleadapter.NewServer(...)`
bekommt den bereits vorhandenen `tenantVerifier`
(`tenantregistryclient.Client`, zuvor schon fuer `backchannel`
verdrahtet) als vierten Parameter mit.
- `dms/internal/workflowevent/workflowevent.go` (INT-13, der reale
Aufrufer): `Config` und `registerRequest` bekommen `TenantSlug`,
`Register` schickt es mit. Ohne diese Ergaenzung haette der neue,
serverseitig *pflichtige* `tenant_slug` den bestehenden,
unveraenderten DMS-Registrierungsablauf aus ENG-02 kaputt gemacht
(Regression gegen Pruefung 3) — DMS kannte das Feld bislang gar
nicht.
- `dms/cmd/worker/main.go` und `dms/cmd/app/api.go`: bestehende,
bereits vorhandene `NEXARCH_DMS_WORKER_TENANT_SLUG` /
`NEXARCH_DMS_APP_TENANT_SLUG` Umgebungsvariablen werden jetzt auch in
die `workflowevent.Config` durchgereicht (kein neuer Zugangsdaten-
Parameter noetig).
- Mail hat noch keinen eigenen Registrierungs-Client im Repo (kein
Pendant zu `dms/internal/workflowevent` vorhanden) — nichts zu
aendern, aber jeder zukuenftige Mail-Adapter muss `tenant_slug`
ebenso mitschicken.
## Pruefungen
### Pruefung 1: Registrierung mit gueltigem, existierendem Tenant-Kontext wird uebernommen
Ergebnis: PASS
- `TestHandleRegister_ValidTenantContextAccepted`
(workflow/internal/moduleadapter): `tenant_slug=mandant-a` gegen einen
fuer `mandant-a` konfigurierten Server, Core-Fake bestaetigt
`mandant-a` -> HTTP 200.
- `TestHandleRegisterAndList_EndToEnd` (angepasst, jetzt mit
`tenant_slug`) zeigt zusaetzlich, dass die Registrierung tatsaechlich
in `module_registrations` landet und ueber `/registrations` sichtbar
ist.
- `TestRegister_SendsObjectTypeAndTriggers` (dms/internal/workflowevent,
INT-13-Seite): bestaetigt, dass DMS den `tenant_slug` tatsaechlich im
Request-Body mitschickt (`got.TenantSlug == "mandant-a"`).
### Pruefung 2: Registrierung mit manipuliertem/fremdem Tenant-Kontext wird abgelehnt
Ergebnis: PASS, drei Varianten abgedeckt:
- `TestHandleRegister_ForeignTenantSlugRejected`: `tenant_slug` weicht
von der Server-Instanz ab (`mandant-fremd` statt `mandant-a`) -> HTTP
403, Log-Zeile `registrierung abgelehnt, unbekannter tenant-kontext`,
anschliessend `TriggersFor` bestaetigt, dass nichts uebernommen wurde.
- `TestHandleRegister_TenantUnknownToCoreRegistryRejected`: der
`tenant_slug` passt formal zur Server-Instanz (`mandant-a`), aber die
(simulierte) Core-Registry kennt diesen Tenant nicht (mehr) -> HTTP
403. Dieser Fall beweist, dass die Pruefung nicht nur lokal gegen die
Instanz-Konfiguration geht (das waere kein unabhaengiger Abgleich),
sondern wirklich gegen Core TEN-01.
- `TestHandleRegister_TenantRegistryCallFailsRejected`: Core-Aufruf
schlaegt technisch fehl (simulierter Transportfehler) -> HTTP 403,
fail closed statt stillschweigend akzeptiert.
### Pruefung 3 (Regression): normaler DMS-/Mail-Registrierungsablauf bleibt funktionsfaehig
Ergebnis: PASS
- Alle bestehenden ENG-02-Tests (`TestRegister_TwoFictionalModulesRegisterIndependently`,
`TestRegister_DuplicateSameTriggersRejected`,
`TestRegister_ChangedTriggersCreateNewVersionAndRollbackRestoresPrevious`,
`TestDeregister_RemovesTriggersCompletely`,
`TestTriggersFor_UnknownObjectTypeReturnsClearError`,
`TestRollback_UnknownVersionReturnsError`) unveraendert gruen — sie
rufen `Register`/`Rollback`/`Deregister` direkt auf Go-Ebene auf, die
DB-Funktionen selbst sind unangetastet.
- Bestehende ENG-09-Tests (`TestPublishEvent_*`) weiterhin gruen,
`/events` ist von diesem Ticket nicht betroffen.
- Kompletter `workflow`-Modultest (`go test ./...`, alle Pakete)
gruen, `go vet ./...` ohne Befund.
- `dms`-Modul: `internal/workflowevent`-Tests gruen (inkl. der beiden
Integrationstests mit echter Jobqueue), `go build ./...` und
`go vet ./...` fuer das komplette Modul fehlerfrei, `cmd/app` und
`cmd/worker` bauen mit der erweiterten `workflowevent.Config`.
## Testbefehl
```
TEST_TENANT_DSN='postgresql://<rolle>:<pw>@localhost:5432/<db>?sslmode=disable' \
go test ./... -p 1
```
(ausgefuehrt sowohl im `workflow`- als auch im `dms`-Modul, jeweils
gegen ein frisches Wegwerf-Schema ueber `internal/testdb`)
## Nicht Bestandteil (laut Ticket / kleinste Loesung)
- `/rollback`, `/deregister`, `/registrations` (GET) bleiben ohne
eigene Tenant-Pruefung — sie wirken auf bereits bestehende,
modul-/objekttyp-identifizierte Registrierungen, nicht auf eine neue
Registrierung mit fremdem Tenant-Claim; AK/Ticket beziehen sich nur
auf die Registrierung selbst.
- `/events` (`PublishEvent`, ENG-09) bleibt wie vor diesem Ticket ohne
Core-Registry-Abgleich (nur lokaler `ownTenantSlug`-Vergleich) — das
ist eine vorbestehende, separate Luecke aus ENG-09 und nicht
Gegenstand von ENG-06 (kein Umbau angrenzender Bereiche).
- Kein eigener Mail-Registrierungs-Client — existiert im Repo noch
nicht, also nichts zu migrieren.
+40 -2
View File
@@ -1,8 +1,10 @@
package moduleadapter
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"time"
@@ -11,6 +13,14 @@ import (
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/svcauth"
)
// TenantVerifier prueft einen vom aufrufenden Modul mitgelieferten
// Tenant-Kontext unabhaengig gegen die Core-Tenant-Registry (TEN-01) -
// ENG-06: Workflow vertraut dabei nicht der Angabe von DMS/Mail selbst
// (Muster aus archive/internal/moduleadapter, RET-12).
type TenantVerifier interface {
VerifyTenant(ctx context.Context, tenantSlug string) (bool, error)
}
// Server bindet die Registrierungs-API an einen Postgres-Pool und
// verlangt ein Service-Token bei jedem Aufruf (fail closed: ein leeres
// oder falsches Token wird immer abgewiesen, Lehre aus RET-14/F-08).
@@ -21,10 +31,11 @@ type Server struct {
pool *pgxpool.Pool
serviceToken string
ownTenantSlug string
tenants TenantVerifier
}
func NewServer(pool *pgxpool.Pool, serviceToken, ownTenantSlug string) *Server {
return &Server{pool: pool, serviceToken: serviceToken, ownTenantSlug: ownTenantSlug}
func NewServer(pool *pgxpool.Pool, serviceToken, ownTenantSlug string, tenants TenantVerifier) *Server {
return &Server{pool: pool, serviceToken: serviceToken, ownTenantSlug: ownTenantSlug, tenants: tenants}
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
@@ -46,10 +57,32 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
}
}
// tenantValid prueft den mitgelieferten Tenant-Kontext unabhaengig von
// der Angabe des registrierenden Moduls (ENG-06 Akzeptanzkriterium 1):
// er muss zu dieser Server-Instanz passen UND von der Core-Tenant-
// Registry als echter, gefuehrter Tenant bestaetigt werden. Ein
// fehlgeschlagener Core-Aufruf gilt als ungueltig (fail closed), kein
// stillschweigendes Durchwinken.
func (s *Server) tenantValid(ctx context.Context, tenantSlug string) bool {
if tenantSlug == "" || tenantSlug != s.ownTenantSlug {
return false
}
if s.tenants == nil {
return true
}
ok, err := s.tenants.VerifyTenant(ctx, tenantSlug)
if err != nil {
log.Printf("moduleadapter: tenant-registry-abgleich fehlgeschlagen tenant=%q: %v", tenantSlug, err)
return false
}
return ok
}
type registerRequest struct {
ModuleName string `json:"module_name"`
ObjectType string `json:"object_type"`
Triggers []string `json:"triggers"`
TenantSlug string `json:"tenant_slug"`
}
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
@@ -58,6 +91,11 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper")
return
}
if !s.tenantValid(r.Context(), req.TenantSlug) {
log.Printf("moduleadapter: registrierung abgelehnt, unbekannter tenant-kontext modul=%s objekttyp=%s tenant=%q", req.ModuleName, req.ObjectType, req.TenantSlug)
writeError(w, http.StatusForbidden, "unbekannter tenant-kontext")
return
}
reg, err := Register(r.Context(), s.pool, req.ModuleName, req.ObjectType, req.Triggers)
if errors.Is(err, ErrDuplicateTriggers) {
writeError(w, http.StatusConflict, "registrierung unveraendert, keine neue version noetig")
+129 -3
View File
@@ -2,17 +2,34 @@ 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"
return NewServer(pool, token, "mandant-a"), token
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
return NewServer(pool, token, "mandant-a", verifier), token
}
func TestHandleRegister_RequiresServiceToken(t *testing.T) {
@@ -22,7 +39,7 @@ func TestHandleRegister_RequiresServiceToken(t *testing.T) {
ts := httptest.NewServer(mux)
defer ts.Close()
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}})
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))
@@ -68,7 +85,7 @@ func TestHandleRegisterAndList_EndToEnd(t *testing.T) {
ts := httptest.NewServer(mux)
defer ts.Close()
body, _ := json.Marshal(registerRequest{ModuleName: "mail-fiktiv", ObjectType: "message", Triggers: []string{"mail.received"}})
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")
@@ -96,3 +113,112 @@ func TestHandleRegisterAndList_EndToEnd(t *testing.T) {
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)
}
}