Files
patrickandClaude Sonnet 5 f6ca199261
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
QA-15 F-08/F-23: Archive /register mit Dienstauthentifizierung und callback_url-Pruefung, Service-Token verpflichtend
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:07:18 +02:00

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