Files
patrickandClaude Sonnet 5 32d222a814
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 / 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
CMP-05: Debug-Ausgabe entfernt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 00:18:12 +02:00

216 lines
7.7 KiB
Go

package complianceapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
)
const testServiceToken = "test-service-token"
// fakeRBACServer simuliert Core RBAC-06 (POST /authorize), Muster wie
// retentionapi/authz_test.go.
func fakeRBACServer(t *testing.T, allowRole string) *rbacclient.Client {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("POST /authorize", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Service-Token") != testServiceToken {
http.Error(w, "kein service-token", http.StatusUnauthorized)
return
}
var req struct {
Role string `json:"role"`
Permission string `json:"permission"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
allowed := req.Role == allowRole && req.Permission == compliancePermission
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allowed})
})
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
return rbacclient.New(server.URL, testServiceToken)
}
func newMux(t *testing.T, allowRole string) (*http.ServeMux, *httptest.Server) {
t.Helper()
pool := requireTestPool(t)
rbac := fakeRBACServer(t, allowRole)
mux := http.NewServeMux()
Mount(mux, pool, rbac)
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
return mux, server
}
// TestRequestHandler_GoBD_TriggersRealExport ist Pflichtpruefung 1: die
// Anforderung eines Berichts loest den zugehoerigen Backend-Export
// tatsaechlich aus (kein Platzhalter-Inhalt).
func TestRequestHandler_GoBD_TriggersRealExport(t *testing.T) {
_, server := newMux(t, "archive_admin")
body, _ := json.Marshal(map[string]string{"report_type": "gobd"})
req, _ := http.NewRequest(http.MethodPost, server.URL+"/compliance/reports", bytes.NewReader(body))
req.Header.Set("X-User-Role", "archive_admin")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("post: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Fatalf("erwartet 200, habe %d", resp.StatusCode)
}
var out requestResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode: %v", err)
}
if out.ID == "" || out.DownloadToken == "" || out.Filename == "" {
t.Fatalf("unvollstaendige antwort: %+v", out)
}
// Der Download muss ein echtes PDF liefern (magic bytes "%PDF"),
// nicht nur eine leere Huelle.
downloadReq, _ := http.NewRequest(http.MethodGet, server.URL+"/compliance/reports/"+out.ID+"/download?token="+out.DownloadToken, nil)
downloadReq.Header.Set("X-User-Role", "archive_admin")
downloadResp, err := http.DefaultClient.Do(downloadReq)
if err != nil {
t.Fatalf("download: %v", err)
}
defer func() { _ = downloadResp.Body.Close() }()
if downloadResp.StatusCode != http.StatusOK {
t.Fatalf("erwartet 200 beim download, habe %d", downloadResp.StatusCode)
}
buf := make([]byte, 4)
if _, err := downloadResp.Body.Read(buf); err != nil {
t.Fatalf("body lesen: %v", err)
}
if string(buf) != "%PDF" {
t.Fatalf("erwartet echten PDF-Export, habe magic bytes %q", buf)
}
}
// TestRequestHandler_DSGVO_RequiresDataSubjectRef und die Historie
// decken das DSGVO-CSV ab.
func TestRequestHandler_DSGVO_ProducesCSV(t *testing.T) {
_, server := newMux(t, "archive_admin")
body, _ := json.Marshal(map[string]string{"report_type": "dsgvo", "data_subject_ref": "person-xyz"})
req, _ := http.NewRequest(http.MethodPost, server.URL+"/compliance/reports", bytes.NewReader(body))
req.Header.Set("X-User-Role", "archive_admin")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("post: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Fatalf("erwartet 200, habe %d", resp.StatusCode)
}
}
// TestRequestHandler_ForbiddenRoleGetsNoAccess ist Pflichtpruefung 2:
// eine nicht berechtigte Rolle erhaelt keinen Zugriff, weder auf das
// Anfordern noch auf die Historie fremder Berichte.
func TestRequestHandler_ForbiddenRoleGetsNoAccess(t *testing.T) {
_, server := newMux(t, "archive_admin")
req, _ := http.NewRequest(http.MethodGet, server.URL+"/compliance/reports", nil)
req.Header.Set("X-User-Role", "normaler_nutzer")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("get: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("erwartet 403 fuer nicht berechtigte rolle, habe %d", resp.StatusCode)
}
postReq, _ := http.NewRequest(http.MethodPost, server.URL+"/compliance/reports", bytes.NewReader([]byte(`{"report_type":"gobd"}`)))
postReq.Header.Set("X-User-Role", "normaler_nutzer")
postResp, err := http.DefaultClient.Do(postReq)
if err != nil {
t.Fatalf("post: %v", err)
}
defer func() { _ = postResp.Body.Close() }()
if postResp.StatusCode != http.StatusForbidden {
t.Fatalf("erwartet 403 beim anfordern durch nicht berechtigte rolle, habe %d", postResp.StatusCode)
}
}
// TestDownloadHandler_ExpiredLinkIsRejected ist Pflichtpruefung 3: der
// Downloadlink eines abgelaufenen Berichts wird abgewiesen. Erzeugt den
// Bericht direkt ueber den Store (nicht ueber HTTP), um eine bereits
// abgelaufene expires_at zu erzwingen, statt 7 Tage zu warten.
func TestDownloadHandler_ExpiredLinkIsRejected(t *testing.T) {
pool := requireTestPool(t)
rbac := fakeRBACServer(t, "archive_admin")
mux := http.NewServeMux()
Mount(mux, pool, rbac)
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
plain, hash, err := newDownloadToken()
if err != nil {
t.Fatalf("newDownloadToken: %v", err)
}
var id string
err = pool.QueryRow(context.Background(), `
INSERT INTO compliance_reports (report_type, requested_by, requested_at, expires_at, content, content_type, filename, download_token_hash)
VALUES ('gobd', 'pruefer_c', now(), now() - interval '1 hour', $1, 'application/pdf', 'abgelaufen.pdf', $2)
RETURNING id
`, []byte("inhalt"), hash).Scan(&id)
if err != nil {
t.Fatalf("abgelaufenen bericht einfuegen: %v", err)
}
req, _ := http.NewRequest(http.MethodGet, server.URL+"/compliance/reports/"+id+"/download?token="+plain, nil)
req.Header.Set("X-User-Role", "archive_admin")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("download: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusGone {
t.Fatalf("erwartet 410 fuer abgelaufenen link, habe %d", resp.StatusCode)
}
}
// TestDownloadHandler_WrongTokenIsRejected: falsches Token darf den
// Bericht nicht ausliefern, unabhaengig vom Ablauf.
func TestDownloadHandler_WrongTokenIsRejected(t *testing.T) {
_, server := newMux(t, "archive_admin")
body, _ := json.Marshal(map[string]string{"report_type": "gobd"})
req, _ := http.NewRequest(http.MethodPost, server.URL+"/compliance/reports", bytes.NewReader(body))
req.Header.Set("X-User-Role", "archive_admin")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("post: %v", err)
}
defer func() { _ = resp.Body.Close() }()
var out requestResponse
_ = json.NewDecoder(resp.Body).Decode(&out)
downloadReq, _ := http.NewRequest(http.MethodGet, server.URL+"/compliance/reports/"+out.ID+"/download?token=falsches-token", nil)
downloadReq.Header.Set("X-User-Role", "archive_admin")
downloadResp, err := http.DefaultClient.Do(downloadReq)
if err != nil {
t.Fatalf("download: %v", err)
}
defer func() { _ = downloadResp.Body.Close() }()
if downloadResp.StatusCode != http.StatusForbidden {
t.Fatalf("erwartet 403 fuer falsches token, habe %d", downloadResp.StatusCode)
}
}