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