CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
Archive-Schnittstellen-Vertragstests / contract-tests (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 / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
288 lines
11 KiB
Go
288 lines
11 KiB
Go
package moduleadapter
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fakeVerifier simuliert Core TEN-01 fuer die Handler-Tests, ohne einen
|
|
// echten Core-Dienst zu benoetigen.
|
|
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
|
|
}
|
|
|
|
// TestRegisterHandler_RealHTTPRoundTrip beweist die REST-Schnittstelle
|
|
// (Ticket-Technikvorgabe) end-to-end: echter HTTP-Server, echter POST,
|
|
// zweiter POST mit abweichender callback_url aendert nichts (Pruefung 3
|
|
// auch ueber die HTTP-Schicht, nicht nur die Go-Funktion direkt).
|
|
func TestRegisterHandler_RealHTTPRoundTrip(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
post := func(body registerRequest) (int, Registration) {
|
|
t.Helper()
|
|
data, _ := json.Marshal(body)
|
|
resp, err := http.Post(server.URL, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
var reg Registration
|
|
if resp.StatusCode == http.StatusOK {
|
|
if err := json.NewDecoder(resp.Body).Decode(®); err != nil {
|
|
t.Fatalf("antwort dekodieren: %v", err)
|
|
}
|
|
}
|
|
return resp.StatusCode, reg
|
|
}
|
|
|
|
status1, reg1 := post(registerRequest{TenantSlug: "mandant-a", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-original", CallbackURL: "https://dms.example.test/original"})
|
|
if status1 != http.StatusOK {
|
|
t.Fatalf("erster post: status = %d, want 200", status1)
|
|
}
|
|
|
|
status2, reg2 := post(registerRequest{TenantSlug: "mandant-a", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-ANDERE", CallbackURL: "https://dms.example.test/andere"})
|
|
if status2 != http.StatusOK {
|
|
t.Fatalf("zweiter post: status = %d, want 200", status2)
|
|
}
|
|
if reg2.ID != reg1.ID || reg2.CallbackURL != "https://dms.example.test/original" {
|
|
t.Fatalf("zweiter post veraenderte bestehenden zustand: %+v, erster war %+v", reg2, reg1)
|
|
}
|
|
}
|
|
|
|
// TestRegisterHandler_RejectsMissingFields ist Nachweis des
|
|
// Fehlerverhaltens auf der REST-Schicht.
|
|
func TestRegisterHandler_RejectsMissingFields(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
data, _ := json.Marshal(registerRequest{TenantSlug: "mandant-a", ModuleName: "dms"})
|
|
resp, err := http.Post(server.URL, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400 bei fehlenden pflichtfeldern", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// TestRegisterHandler_AcceptsValidTenantContext ist Akzeptanzkriterium 1
|
|
// (Prüfung 1): Registrierung mit gültigem, existierendem Tenant-Kontext
|
|
// wird übernommen - die Verifikation gegen die Core-Registry blockiert
|
|
// den regulären Ablauf nicht.
|
|
func TestRegisterHandler_AcceptsValidTenantContext(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
data, _ := json.Marshal(registerRequest{TenantSlug: "mandant-a", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-standard", CallbackURL: "https://dms.example.test/callback"})
|
|
resp, err := http.Post(server.URL, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 bei gueltigem tenant-kontext", resp.StatusCode)
|
|
}
|
|
|
|
all, err := ListRegistrations(context.Background(), pool)
|
|
if err != nil {
|
|
t.Fatalf("listregistrations: %v", err)
|
|
}
|
|
if len(all) != 1 || all[0].TenantSlug != "mandant-a" {
|
|
t.Fatalf("erwartet genau 1 registrierung mit tenant_slug=mandant-a, habe %+v", all)
|
|
}
|
|
}
|
|
|
|
// TestRegisterHandler_RejectsUnknownTenantContext ist Akzeptanzkriterium
|
|
// 2 (Prüfung 2): eine Registrierung mit einem nicht existierenden oder
|
|
// gefälschten Tenant-Kontext wird abgelehnt, statt uebernommen zu werden.
|
|
func TestRegisterHandler_RejectsUnknownTenantContext(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
data, _ := json.Marshal(registerRequest{TenantSlug: "fremder-mandant", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-standard", CallbackURL: "https://dms.example.test/callback"})
|
|
resp, err := http.Post(server.URL, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403 bei unbekanntem tenant-kontext", resp.StatusCode)
|
|
}
|
|
|
|
all, err := ListRegistrations(context.Background(), pool)
|
|
if err != nil {
|
|
t.Fatalf("listregistrations: %v", err)
|
|
}
|
|
if len(all) != 0 {
|
|
t.Fatalf("abgelehnte registrierung wurde dennoch uebernommen: %+v", all)
|
|
}
|
|
}
|
|
|
|
// TestRegisterHandler_TenantVerificationErrorFailsClosed stellt sicher,
|
|
// dass ein Transportfehler bei der Core-Registry-Abfrage NICHT
|
|
// stillschweigend als gueltig behandelt wird.
|
|
func TestRegisterHandler_TenantVerificationErrorFailsClosed(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{err: http.ErrServerClosed}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
data, _ := json.Marshal(registerRequest{TenantSlug: "mandant-a", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-standard", CallbackURL: "https://dms.example.test/callback"})
|
|
resp, err := http.Post(server.URL, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode == http.StatusOK {
|
|
t.Fatal("fehler bei tenant-verifikation wurde faelschlich als gueltig behandelt (fail-open)")
|
|
}
|
|
|
|
all, err := ListRegistrations(context.Background(), pool)
|
|
if err != nil {
|
|
t.Fatalf("listregistrations: %v", err)
|
|
}
|
|
if len(all) != 0 {
|
|
t.Fatalf("registrierung trotz verifikationsfehler uebernommen: %+v", all)
|
|
}
|
|
}
|
|
|
|
// testRegisterConfig: Tests ohne DNS und ohne Service-Token; Rechnernamen
|
|
// loesen auf eine oeffentliche Adresse auf.
|
|
func testRegisterConfig() RegisterConfig {
|
|
return RegisterConfig{
|
|
AllowUnauthenticated: true,
|
|
Callback: CallbackPolicy{Resolve: func(context.Context, string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("93.184.216.34")}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
func postRegister(t *testing.T, url string, header http.Header, body any) (int, string) {
|
|
t.Helper()
|
|
data, _ := json.Marshal(body)
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
|
|
for k, v := range header {
|
|
req.Header[k] = v
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(resp.Body)
|
|
return resp.StatusCode, buf.String()
|
|
}
|
|
|
|
func validRegisterBody() registerRequest {
|
|
return registerRequest{TenantSlug: "mandant-a", ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-original", CallbackURL: "https://dms.example.test/original"}
|
|
}
|
|
|
|
// QA-15 F-08: ohne Token (und ohne ausdrueckliche Freigabe) registriert /register nichts.
|
|
func TestRegisterHandler_RequiresServiceToken(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
cfg := testRegisterConfig()
|
|
|
|
closedCfg := cfg
|
|
closedCfg.AllowUnauthenticated = false
|
|
closed := httptest.NewServer(RegisterHandler(pool, verifier, closedCfg))
|
|
defer closed.Close()
|
|
if code, _ := postRegister(t, closed.URL, nil, validRegisterBody()); code != http.StatusServiceUnavailable {
|
|
t.Errorf("ohne token-konfiguration: status = %d, want 503", code)
|
|
}
|
|
|
|
tokCfg := cfg
|
|
tokCfg.ServiceToken = "geheim"
|
|
tokCfg.AllowUnauthenticated = false
|
|
secured := httptest.NewServer(RegisterHandler(pool, verifier, tokCfg))
|
|
defer secured.Close()
|
|
for name, hdr := range map[string]http.Header{
|
|
"kein token": nil,
|
|
"falsches token": {HeaderServiceToken: {"falsch"}},
|
|
"praefix": {HeaderServiceToken: {"geheim-und-mehr"}},
|
|
"leer": {HeaderServiceToken: {""}},
|
|
} {
|
|
if code, _ := postRegister(t, secured.URL, hdr, validRegisterBody()); code != http.StatusUnauthorized {
|
|
t.Errorf("%s: status = %d, want 401", name, code)
|
|
}
|
|
}
|
|
regs, err := ListRegistrations(context.Background(), pool)
|
|
if err != nil || len(regs) != 0 {
|
|
t.Fatalf("abgelehnte anfragen haben registrierungen angelegt: %v %v", regs, err)
|
|
}
|
|
if code, _ := postRegister(t, secured.URL, http.Header{HeaderServiceToken: {"geheim"}}, validRegisterBody()); code != http.StatusOK {
|
|
t.Errorf("richtiges token: status = %d, want 200", code)
|
|
}
|
|
}
|
|
|
|
// QA-15 F-08 (SSRF): unzulaessige Rueckruf-Adressen werden mit 400 abgewiesen und nicht gespeichert.
|
|
func TestRegisterHandler_RejectsUnsafeCallbackURLs(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
for _, u := range []string{
|
|
"http://dms.example.test/x",
|
|
"https://127.0.0.1/x",
|
|
"https://[::1]/x",
|
|
"https://169.254.169.254/latest/meta-data",
|
|
"https://10.0.0.5/x",
|
|
"https://192.168.1.10:8443/x",
|
|
"file:///etc/passwd",
|
|
"gopher://dms.example.test/",
|
|
"https://user:pw@dms.example.test/x",
|
|
"https://localhost/x",
|
|
"https:///nohost",
|
|
"https://dms.example.test:99999/x",
|
|
"https://dms.example.test/x y",
|
|
} {
|
|
body := validRegisterBody()
|
|
body.CallbackURL = u
|
|
if code, _ := postRegister(t, server.URL, nil, body); code != http.StatusBadRequest {
|
|
t.Errorf("callback_url %q: status = %d, want 400", u, code)
|
|
}
|
|
}
|
|
if regs, _ := ListRegistrations(context.Background(), pool); len(regs) != 0 {
|
|
t.Fatalf("abgelehnte callback_urls wurden gespeichert: %v", regs)
|
|
}
|
|
}
|
|
|
|
func TestRegisterHandler_BodyLimit(t *testing.T) {
|
|
pool := requireTestPool(t)
|
|
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
|
server := httptest.NewServer(RegisterHandler(pool, verifier, testRegisterConfig()))
|
|
defer server.Close()
|
|
|
|
big := validRegisterBody()
|
|
big.RetentionClass = strings.Repeat("x", maxRegisterBodyBytes+1)
|
|
if code, _ := postRegister(t, server.URL, nil, big); code != http.StatusBadRequest {
|
|
t.Errorf("uebergrosser body: status = %d, want 400", code)
|
|
}
|
|
}
|