Files
breakpilot-compliance/backend-compliance/tests/test_dsr_routes.py
Sharang Parnerkar 3320ef94fc refactor: phase 0 guardrails + phase 1 step 2 (models.py split)
Squash of branch refactor/phase0-guardrails-and-models-split — 4 commits,
81 files, 173/173 pytest green, OpenAPI contract preserved (360 paths /
484 operations).

## Phase 0 — Architecture guardrails

Three defense-in-depth layers to keep the architecture rules enforced
regardless of who opens Claude Code in this repo:

  1. .claude/settings.json PreToolUse hook on Write/Edit blocks any file
     that would exceed the 500-line hard cap. Auto-loads in every Claude
     session in this repo.
  2. scripts/githooks/pre-commit (install via scripts/install-hooks.sh)
     enforces the LOC cap locally, freezes migrations/ without
     [migration-approved], and protects guardrail files without
     [guardrail-change].
  3. .gitea/workflows/ci.yaml gains loc-budget + guardrail-integrity +
     sbom-scan (syft+grype) jobs, adds mypy --strict for the new Python
     packages (compliance/{services,repositories,domain,schemas}), and
     tsc --noEmit for admin-compliance + developer-portal.

Per-language conventions documented in AGENTS.python.md, AGENTS.go.md,
AGENTS.typescript.md at the repo root — layering, tooling, and explicit
"what you may NOT do" lists. Root CLAUDE.md is prepended with the six
non-negotiable rules. Each of the 10 services gets a README.md.

scripts/check-loc.sh enforces soft 300 / hard 500 and surfaces the
current baseline of 205 hard + 161 soft violations so Phases 1-4 can
drain it incrementally. CI gates only CHANGED files in PRs so the
legacy baseline does not block unrelated work.

## Deprecation sweep

47 files. Pydantic V1 regex= -> pattern= (2 sites), class Config ->
ConfigDict in source_policy_router.py (schemas.py intentionally skipped;
it is the Phase 1 Step 3 split target). datetime.utcnow() ->
datetime.now(timezone.utc) everywhere including SQLAlchemy default=
callables. All DB columns already declare timezone=True, so this is a
latent-bug fix at the Python side, not a schema change.

DeprecationWarning count dropped from 158 to 35.

## Phase 1 Step 1 — Contract test harness

tests/contracts/test_openapi_baseline.py diffs the live FastAPI /openapi.json
against tests/contracts/openapi.baseline.json on every test run. Fails on
removed paths, removed status codes, or new required request body fields.
Regenerate only via tests/contracts/regenerate_baseline.py after a
consumer-updated contract change. This is the safety harness for all
subsequent refactor commits.

## Phase 1 Step 2 — models.py split (1466 -> 85 LOC shim)

compliance/db/models.py is decomposed into seven sibling aggregate modules
following the existing repo pattern (dsr_models.py, vvt_models.py, ...):

  regulation_models.py       (134) — Regulation, Requirement
  control_models.py          (279) — Control, Mapping, Evidence, Risk
  ai_system_models.py        (141) — AISystem, AuditExport
  service_module_models.py   (176) — ServiceModule, ModuleRegulation, ModuleRisk
  audit_session_models.py    (177) — AuditSession, AuditSignOff
  isms_governance_models.py  (323) — ISMSScope, Context, Policy, Objective, SoA
  isms_audit_models.py       (468) — Finding, CAPA, MgmtReview, InternalAudit,
                                     AuditTrail, Readiness

models.py becomes an 85-line re-export shim in dependency order so
existing imports continue to work unchanged. Schema is byte-identical:
__tablename__, column definitions, relationship strings, back_populates,
cascade directives all preserved.

All new sibling files are under the 500-line hard cap; largest is
isms_audit_models.py at 468. No file in compliance/db/ now exceeds
the hard cap.

## Phase 1 Step 3 — infrastructure only

backend-compliance/compliance/{schemas,domain,repositories}/ packages
are created as landing zones with docstrings. compliance/domain/
exports DomainError / NotFoundError / ConflictError / ValidationError /
PermissionError — the base classes services will use to raise
domain-level errors instead of HTTPException.

PHASE1_RUNBOOK.md at backend-compliance/PHASE1_RUNBOOK.md documents
the nine-step execution plan for Phase 1: snapshot baseline,
characterization tests, split models.py (this commit), split schemas.py
(next), extract services, extract repositories, mypy --strict, coverage.

## Verification

  backend-compliance/.venv-phase1: uv python install 3.12 + pip -r requirements.txt
  PYTHONPATH=. pytest compliance/tests/ tests/contracts/
  -> 173 passed, 0 failed, 35 warnings, OpenAPI 360/484 unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:18:29 +02:00

746 lines
27 KiB
Python

"""
Tests for DSR (Data Subject Request) routes.
Pattern: app.dependency_overrides[get_db] for FastAPI DI.
"""
import uuid
import os
import sys
from datetime import datetime, timedelta, timezone
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
# Ensure backend dir is on path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from classroom_engine.database import Base, get_db
from compliance.db.dsr_models import (
DSRRequestDB, DSRStatusHistoryDB, DSRCommunicationDB,
DSRTemplateDB, DSRTemplateVersionDB, DSRExceptionCheckDB,
)
from compliance.api.dsr_routes import router as dsr_router
# In-memory SQLite for testing
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_dsr.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
HEADERS = {"X-Tenant-ID": TENANT_ID}
# Create a minimal test app (avoids importing main.py with its Python 3.10+ syntax issues)
app = FastAPI()
app.include_router(dsr_router, prefix="/api/compliance")
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_db():
"""Create all tables before each test, drop after."""
Base.metadata.create_all(bind=engine)
# Create sequence workaround for SQLite (no sequences)
db = TestingSessionLocal()
try:
# SQLite doesn't have sequences; we'll mock the request number generation
pass
finally:
db.close()
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def db_session():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
def _create_dsr_in_db(db, **kwargs):
"""Helper to create a DSR directly in DB."""
now = datetime.now(timezone.utc)
defaults = {
"tenant_id": uuid.UUID(TENANT_ID),
"request_number": f"DSR-2026-{str(uuid.uuid4())[:6].upper()}",
"request_type": "access",
"status": "intake",
"priority": "normal",
"requester_name": "Max Mustermann",
"requester_email": "max@example.de",
"source": "email",
"received_at": now,
"deadline_at": now + timedelta(days=30),
"created_at": now,
"updated_at": now,
}
defaults.update(kwargs)
dsr = DSRRequestDB(**defaults)
db.add(dsr)
db.commit()
db.refresh(dsr)
return dsr
# =============================================================================
# CREATE Tests
# =============================================================================
class TestCreateDSR:
def test_create_access_request(self, db_session):
resp = client.post("/api/compliance/dsr", json={
"request_type": "access",
"requester_name": "Max Mustermann",
"requester_email": "max@example.de",
"source": "email",
"request_text": "Auskunft nach Art. 15 DSGVO",
}, headers=HEADERS)
# May fail on SQLite due to sequence; check for 200 or 500
if resp.status_code == 200:
data = resp.json()
assert data["request_type"] == "access"
assert data["status"] == "intake"
assert data["requester_name"] == "Max Mustermann"
assert data["requester_email"] == "max@example.de"
assert data["deadline_at"] is not None
def test_create_erasure_request(self, db_session):
resp = client.post("/api/compliance/dsr", json={
"request_type": "erasure",
"requester_name": "Anna Schmidt",
"requester_email": "anna@example.de",
"source": "web_form",
"request_text": "Bitte alle Daten loeschen",
"priority": "high",
}, headers=HEADERS)
if resp.status_code == 200:
data = resp.json()
assert data["request_type"] == "erasure"
assert data["priority"] == "high"
def test_create_invalid_type(self):
resp = client.post("/api/compliance/dsr", json={
"request_type": "invalid_type",
"requester_name": "Test",
"requester_email": "test@test.de",
}, headers=HEADERS)
assert resp.status_code == 400
def test_create_invalid_source(self):
resp = client.post("/api/compliance/dsr", json={
"request_type": "access",
"requester_name": "Test",
"requester_email": "test@test.de",
"source": "invalid_source",
}, headers=HEADERS)
assert resp.status_code == 400
def test_create_invalid_priority(self):
resp = client.post("/api/compliance/dsr", json={
"request_type": "access",
"requester_name": "Test",
"requester_email": "test@test.de",
"priority": "ultra",
}, headers=HEADERS)
assert resp.status_code == 400
def test_create_missing_name(self):
resp = client.post("/api/compliance/dsr", json={
"request_type": "access",
"requester_email": "test@test.de",
}, headers=HEADERS)
assert resp.status_code == 422
def test_create_missing_email(self):
resp = client.post("/api/compliance/dsr", json={
"request_type": "access",
"requester_name": "Test",
}, headers=HEADERS)
assert resp.status_code == 422
# =============================================================================
# LIST Tests
# =============================================================================
class TestListDSR:
def test_list_empty(self):
resp = client.get("/api/compliance/dsr", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["requests"] == []
assert data["total"] == 0
def test_list_with_data(self, db_session):
_create_dsr_in_db(db_session, request_type="access")
_create_dsr_in_db(db_session, request_type="erasure")
resp = client.get("/api/compliance/dsr", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 2
assert len(data["requests"]) == 2
def test_list_filter_by_status(self, db_session):
_create_dsr_in_db(db_session, status="intake")
_create_dsr_in_db(db_session, status="processing")
_create_dsr_in_db(db_session, status="completed")
resp = client.get("/api/compliance/dsr?status=intake", headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["total"] == 1
def test_list_filter_by_type(self, db_session):
_create_dsr_in_db(db_session, request_type="access")
_create_dsr_in_db(db_session, request_type="erasure")
resp = client.get("/api/compliance/dsr?request_type=erasure", headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["total"] == 1
def test_list_filter_by_priority(self, db_session):
_create_dsr_in_db(db_session, priority="high")
_create_dsr_in_db(db_session, priority="normal")
resp = client.get("/api/compliance/dsr?priority=high", headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["total"] == 1
def test_list_search(self, db_session):
_create_dsr_in_db(db_session, requester_name="Max Mustermann", requester_email="max@example.de")
_create_dsr_in_db(db_session, requester_name="Anna Schmidt", requester_email="anna@example.de")
resp = client.get("/api/compliance/dsr?search=Anna", headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["total"] == 1
def test_list_pagination(self, db_session):
for i in range(5):
_create_dsr_in_db(db_session)
resp = client.get("/api/compliance/dsr?limit=2&offset=0", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 5
assert len(data["requests"]) == 2
def test_list_overdue_only(self, db_session):
_create_dsr_in_db(db_session, deadline_at=datetime.now(timezone.utc) - timedelta(days=5), status="processing")
_create_dsr_in_db(db_session, deadline_at=datetime.now(timezone.utc) + timedelta(days=20), status="processing")
resp = client.get("/api/compliance/dsr?overdue_only=true", headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["total"] == 1
# =============================================================================
# GET DETAIL Tests
# =============================================================================
class TestGetDSR:
def test_get_existing(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.get(f"/api/compliance/dsr/{dsr.id}", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == str(dsr.id)
assert data["requester_name"] == "Max Mustermann"
def test_get_nonexistent(self):
fake_id = str(uuid.uuid4())
resp = client.get(f"/api/compliance/dsr/{fake_id}", headers=HEADERS)
assert resp.status_code == 404
def test_get_invalid_id(self):
resp = client.get("/api/compliance/dsr/not-a-uuid", headers=HEADERS)
assert resp.status_code == 400
# =============================================================================
# UPDATE Tests
# =============================================================================
class TestUpdateDSR:
def test_update_priority(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.put(f"/api/compliance/dsr/{dsr.id}", json={
"priority": "high",
}, headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["priority"] == "high"
def test_update_notes(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.put(f"/api/compliance/dsr/{dsr.id}", json={
"notes": "Test note",
"internal_notes": "Internal note",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["notes"] == "Test note"
assert data["internal_notes"] == "Internal note"
def test_update_invalid_priority(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.put(f"/api/compliance/dsr/{dsr.id}", json={
"priority": "ultra",
}, headers=HEADERS)
assert resp.status_code == 400
# =============================================================================
# DELETE Tests
# =============================================================================
class TestDeleteDSR:
def test_cancel_dsr(self, db_session):
dsr = _create_dsr_in_db(db_session, status="intake")
resp = client.delete(f"/api/compliance/dsr/{dsr.id}", headers=HEADERS)
assert resp.status_code == 200
# Verify status is cancelled
resp2 = client.get(f"/api/compliance/dsr/{dsr.id}", headers=HEADERS)
assert resp2.json()["status"] == "cancelled"
def test_cancel_already_completed(self, db_session):
dsr = _create_dsr_in_db(db_session, status="completed")
resp = client.delete(f"/api/compliance/dsr/{dsr.id}", headers=HEADERS)
assert resp.status_code == 400
# =============================================================================
# STATS Tests
# =============================================================================
class TestDSRStats:
def test_stats_empty(self):
resp = client.get("/api/compliance/dsr/stats", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 0
def test_stats_with_data(self, db_session):
_create_dsr_in_db(db_session, status="intake", request_type="access")
_create_dsr_in_db(db_session, status="processing", request_type="erasure")
_create_dsr_in_db(db_session, status="completed", request_type="access",
completed_at=datetime.now(timezone.utc))
resp = client.get("/api/compliance/dsr/stats", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 3
assert data["by_status"]["intake"] == 1
assert data["by_status"]["processing"] == 1
assert data["by_status"]["completed"] == 1
assert data["by_type"]["access"] == 2
assert data["by_type"]["erasure"] == 1
# =============================================================================
# WORKFLOW Tests
# =============================================================================
class TestDSRWorkflow:
def test_change_status(self, db_session):
dsr = _create_dsr_in_db(db_session, status="intake")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/status", json={
"status": "identity_verification",
"comment": "ID angefragt",
}, headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["status"] == "identity_verification"
def test_change_status_invalid(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.post(f"/api/compliance/dsr/{dsr.id}/status", json={
"status": "invalid_status",
}, headers=HEADERS)
assert resp.status_code == 400
def test_verify_identity(self, db_session):
dsr = _create_dsr_in_db(db_session, status="identity_verification")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/verify-identity", json={
"method": "id_document",
"notes": "Personalausweis geprueft",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["identity_verified"] is True
assert data["verification_method"] == "id_document"
assert data["status"] == "processing"
def test_assign_dsr(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.post(f"/api/compliance/dsr/{dsr.id}/assign", json={
"assignee_id": "DSB Mueller",
}, headers=HEADERS)
assert resp.status_code == 200
assert resp.json()["assigned_to"] == "DSB Mueller"
def test_extend_deadline(self, db_session):
dsr = _create_dsr_in_db(db_session, status="processing")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/extend", json={
"reason": "Komplexe Anfrage",
"days": 60,
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["extended_deadline_at"] is not None
assert data["extension_reason"] == "Komplexe Anfrage"
def test_extend_deadline_closed_dsr(self, db_session):
dsr = _create_dsr_in_db(db_session, status="completed")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/extend", json={
"reason": "Test",
}, headers=HEADERS)
assert resp.status_code == 400
def test_complete_dsr(self, db_session):
dsr = _create_dsr_in_db(db_session, status="processing")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/complete", json={
"summary": "Auskunft erteilt",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "completed"
assert data["completed_at"] is not None
def test_complete_already_completed(self, db_session):
dsr = _create_dsr_in_db(db_session, status="completed")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/complete", json={
"summary": "Nochmal",
}, headers=HEADERS)
assert resp.status_code == 400
def test_reject_dsr(self, db_session):
dsr = _create_dsr_in_db(db_session, status="processing")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/reject", json={
"reason": "Unberechtigt",
"legal_basis": "Art. 17(3)(b)",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "rejected"
assert data["rejection_reason"] == "Unberechtigt"
assert data["rejection_legal_basis"] == "Art. 17(3)(b)"
# =============================================================================
# HISTORY & COMMUNICATIONS Tests
# =============================================================================
class TestDSRHistory:
def test_get_history(self, db_session):
dsr = _create_dsr_in_db(db_session)
# Add a history entry
entry = DSRStatusHistoryDB(
tenant_id=uuid.UUID(TENANT_ID),
dsr_id=dsr.id,
previous_status="intake",
new_status="processing",
changed_by="admin",
comment="Test",
)
db_session.add(entry)
db_session.commit()
resp = client.get(f"/api/compliance/dsr/{dsr.id}/history", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["new_status"] == "processing"
class TestDSRCommunications:
def test_send_communication(self, db_session):
dsr = _create_dsr_in_db(db_session)
resp = client.post(f"/api/compliance/dsr/{dsr.id}/communicate", json={
"communication_type": "outgoing",
"channel": "email",
"subject": "Eingangsbestaetigung",
"content": "Ihre Anfrage wurde erhalten.",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["channel"] == "email"
assert data["sent_at"] is not None
def test_get_communications(self, db_session):
dsr = _create_dsr_in_db(db_session)
comm = DSRCommunicationDB(
tenant_id=uuid.UUID(TENANT_ID),
dsr_id=dsr.id,
communication_type="outgoing",
channel="email",
content="Test",
)
db_session.add(comm)
db_session.commit()
resp = client.get(f"/api/compliance/dsr/{dsr.id}/communications", headers=HEADERS)
assert resp.status_code == 200
assert len(resp.json()) == 1
# =============================================================================
# EXCEPTION CHECKS Tests
# =============================================================================
class TestExceptionChecks:
def test_init_exception_checks(self, db_session):
dsr = _create_dsr_in_db(db_session, request_type="erasure")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 5
assert data[0]["check_code"] == "art17_3_a"
def test_init_exception_checks_not_erasure(self, db_session):
dsr = _create_dsr_in_db(db_session, request_type="access")
resp = client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
assert resp.status_code == 400
def test_init_exception_checks_already_initialized(self, db_session):
dsr = _create_dsr_in_db(db_session, request_type="erasure")
# First init
client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
# Second init should fail
resp = client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
assert resp.status_code == 400
def test_update_exception_check(self, db_session):
dsr = _create_dsr_in_db(db_session, request_type="erasure")
init_resp = client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
checks = init_resp.json()
check_id = checks[0]["id"]
resp = client.put(f"/api/compliance/dsr/{dsr.id}/exception-checks/{check_id}", json={
"applies": True,
"notes": "Aufbewahrungspflicht nach HGB",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["applies"] is True
assert data["notes"] == "Aufbewahrungspflicht nach HGB"
def test_get_exception_checks(self, db_session):
dsr = _create_dsr_in_db(db_session, request_type="erasure")
client.post(f"/api/compliance/dsr/{dsr.id}/exception-checks/init", headers=HEADERS)
resp = client.get(f"/api/compliance/dsr/{dsr.id}/exception-checks", headers=HEADERS)
assert resp.status_code == 200
assert len(resp.json()) == 5
# =============================================================================
# DEADLINE PROCESSING Tests
# =============================================================================
class TestDeadlineProcessing:
def test_process_deadlines_empty(self):
resp = client.post("/api/compliance/dsr/deadlines/process", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["processed"] == 0
def test_process_deadlines_with_overdue(self, db_session):
_create_dsr_in_db(db_session, status="processing",
deadline_at=datetime.now(timezone.utc) - timedelta(days=5))
_create_dsr_in_db(db_session, status="processing",
deadline_at=datetime.now(timezone.utc) + timedelta(days=20))
resp = client.post("/api/compliance/dsr/deadlines/process", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["processed"] == 1
# =============================================================================
# TEMPLATE Tests
# =============================================================================
class TestDSRTemplates:
def test_get_templates(self, db_session):
t = DSRTemplateDB(
tenant_id=uuid.UUID(TENANT_ID),
name="Eingangsbestaetigung",
template_type="receipt",
language="de",
)
db_session.add(t)
db_session.commit()
resp = client.get("/api/compliance/dsr/templates", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
def test_get_published_templates(self, db_session):
t = DSRTemplateDB(
tenant_id=uuid.UUID(TENANT_ID),
name="Test",
template_type="receipt",
language="de",
is_active=True,
)
db_session.add(t)
db_session.commit()
db_session.refresh(t)
v = DSRTemplateVersionDB(
template_id=t.id,
version="1.0",
subject="Bestaetigung",
body_html="<p>Test</p>",
status="published",
published_at=datetime.now(timezone.utc),
)
db_session.add(v)
db_session.commit()
resp = client.get("/api/compliance/dsr/templates/published", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
assert data[0]["latest_version"] is not None
def test_create_template_version(self, db_session):
t = DSRTemplateDB(
tenant_id=uuid.UUID(TENANT_ID),
name="Test",
template_type="receipt",
language="de",
)
db_session.add(t)
db_session.commit()
db_session.refresh(t)
resp = client.post(f"/api/compliance/dsr/templates/{t.id}/versions", json={
"version": "1.0",
"subject": "Bestaetigung {{referenceNumber}}",
"body_html": "<p>Ihre Anfrage wurde erhalten.</p>",
}, headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["version"] == "1.0"
assert data["status"] == "draft"
def test_publish_template_version(self, db_session):
t = DSRTemplateDB(
tenant_id=uuid.UUID(TENANT_ID),
name="Test",
template_type="receipt",
language="de",
)
db_session.add(t)
db_session.commit()
db_session.refresh(t)
v = DSRTemplateVersionDB(
template_id=t.id,
version="1.0",
subject="Test",
body_html="<p>Test</p>",
status="draft",
)
db_session.add(v)
db_session.commit()
db_session.refresh(v)
resp = client.put(f"/api/compliance/dsr/template-versions/{v.id}/publish", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "published"
assert data["published_at"] is not None
def test_get_template_versions(self, db_session):
t = DSRTemplateDB(
tenant_id=uuid.UUID(TENANT_ID),
name="Test",
template_type="receipt",
language="de",
)
db_session.add(t)
db_session.commit()
db_session.refresh(t)
v = DSRTemplateVersionDB(
template_id=t.id,
version="1.0",
subject="V1",
body_html="<p>V1</p>",
)
db_session.add(v)
db_session.commit()
resp = client.get(f"/api/compliance/dsr/templates/{t.id}/versions", headers=HEADERS)
assert resp.status_code == 200
assert len(resp.json()) == 1
def test_get_template_versions_not_found(self):
fake_id = str(uuid.uuid4())
resp = client.get(f"/api/compliance/dsr/templates/{fake_id}/versions", headers=HEADERS)
assert resp.status_code == 404
class TestDSRExport:
"""Tests for DSR export endpoint."""
def test_export_csv_empty(self):
resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
assert resp.status_code == 200
assert "text/csv" in resp.headers.get("content-type", "")
lines = resp.text.strip().split("\n")
assert len(lines) == 1 # Header only
assert "Referenznummer" in lines[0]
assert "Zugewiesen" in lines[0]
def test_export_csv_with_data(self):
# Create a DSR first
body = {
"request_type": "access",
"requester_name": "Export Test",
"requester_email": "export@example.de",
"source": "email",
}
create_resp = client.post("/api/compliance/dsr", json=body, headers=HEADERS)
assert create_resp.status_code == 200
resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
assert resp.status_code == 200
lines = resp.text.strip().split("\n")
assert len(lines) >= 2 # Header + at least 1 data row
# Check data row contains our test data
assert "Export Test" in lines[1]
assert "export@example.de" in lines[1]
assert "access" in lines[1]
def test_export_json(self):
resp = client.get("/api/compliance/dsr/export?format=json", headers=HEADERS)
assert resp.status_code == 200
data = resp.json()
assert "exported_at" in data
assert "total" in data
assert "requests" in data
assert isinstance(data["requests"], list)
def test_export_invalid_format(self):
resp = client.get("/api/compliance/dsr/export?format=xml", headers=HEADERS)
assert resp.status_code == 422 # Validation error