This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_dsr_api.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

424 lines
14 KiB
Python

"""
Tests für die DSR (Data Subject Request) API
Testet die Proxy-Endpoints für Betroffenenanfragen.
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from fastapi.testclient import TestClient
import httpx
class TestDSRUserAPI:
"""Tests für User-Endpoints der DSR API."""
def test_create_dsr_request_body(self):
"""Test: CreateDSRRequest Model Validierung."""
from dsr_api import CreateDSRRequest
# Valide Anfrage
req = CreateDSRRequest(
request_type="access",
requester_email="test@example.com",
requester_name="Max Mustermann"
)
assert req.request_type == "access"
assert req.requester_email == "test@example.com"
# Minimal-Anfrage
req_minimal = CreateDSRRequest(
request_type="erasure"
)
assert req_minimal.request_type == "erasure"
assert req_minimal.requester_email is None
def test_valid_request_types(self):
"""Test: Alle DSGVO-Anfragetypen."""
valid_types = ["access", "rectification", "erasure", "restriction", "portability"]
for req_type in valid_types:
from dsr_api import CreateDSRRequest
req = CreateDSRRequest(request_type=req_type)
assert req.request_type == req_type
@pytest.mark.asyncio
async def test_proxy_request_success(self):
"""Test: Erfolgreiche Proxy-Anfrage."""
from dsr_api import proxy_request
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.content = b'{"success": true}'
mock_response.json.return_value = {"success": True}
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.get = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_instance
result = await proxy_request("GET", "/dsr", "test-token")
assert result == {"success": True}
@pytest.mark.asyncio
async def test_proxy_request_error(self):
"""Test: Fehler bei Proxy-Anfrage."""
from dsr_api import proxy_request
from fastapi import HTTPException
mock_response = MagicMock()
mock_response.status_code = 404
mock_response.content = b'{"error": "Not found"}'
mock_response.json.return_value = {"error": "Not found"}
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.get = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_instance
with pytest.raises(HTTPException) as exc_info:
await proxy_request("GET", "/dsr/invalid-id", "test-token")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_proxy_request_service_unavailable(self):
"""Test: Consent Service nicht erreichbar."""
from dsr_api import proxy_request
from fastapi import HTTPException
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.get = AsyncMock(side_effect=httpx.RequestError("Connection failed"))
mock_client.return_value.__aenter__.return_value = mock_instance
with pytest.raises(HTTPException) as exc_info:
await proxy_request("GET", "/dsr", "test-token")
assert exc_info.value.status_code == 503
def test_get_token_valid(self):
"""Test: Token-Extraktion aus Header."""
from dsr_api import get_token
token = get_token("Bearer valid-jwt-token")
assert token == "valid-jwt-token"
def test_get_token_invalid(self):
"""Test: Ungültiger Authorization Header."""
from dsr_api import get_token
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc_info:
get_token(None)
assert exc_info.value.status_code == 401
with pytest.raises(HTTPException) as exc_info:
get_token("InvalidHeader")
assert exc_info.value.status_code == 401
class TestDSRAdminAPI:
"""Tests für Admin-Endpoints der DSR API."""
def test_create_dsr_admin_request_body(self):
"""Test: Admin CreateDSRRequest Model."""
from dsr_admin_api import CreateDSRRequest
req = CreateDSRRequest(
request_type="erasure",
requester_email="user@example.com",
requester_name="Test User",
priority="high",
source="admin_panel"
)
assert req.request_type == "erasure"
assert req.requester_email == "user@example.com"
assert req.priority == "high"
assert req.source == "admin_panel"
def test_update_dsr_request_body(self):
"""Test: UpdateDSRRequest Model."""
from dsr_admin_api import UpdateDSRRequest
req = UpdateDSRRequest(
status="processing",
priority="expedited",
processing_notes="Daten werden zusammengestellt"
)
assert req.status == "processing"
assert req.priority == "expedited"
assert req.processing_notes == "Daten werden zusammengestellt"
def test_verify_identity_request_body(self):
"""Test: VerifyIdentityRequest Model."""
from dsr_admin_api import VerifyIdentityRequest
req = VerifyIdentityRequest(method="id_card")
assert req.method == "id_card"
req2 = VerifyIdentityRequest(method="video_call")
assert req2.method == "video_call"
def test_extend_deadline_request_body(self):
"""Test: ExtendDeadlineRequest Model."""
from dsr_admin_api import ExtendDeadlineRequest
req = ExtendDeadlineRequest(
reason="Komplexität der Anfrage",
days=60
)
assert req.reason == "Komplexität der Anfrage"
assert req.days == 60
def test_complete_dsr_request_body(self):
"""Test: CompleteDSRRequest Model."""
from dsr_admin_api import CompleteDSRRequest
req = CompleteDSRRequest(
summary="Alle Daten wurden bereitgestellt.",
result_data={"files": ["export.json"]}
)
assert req.summary == "Alle Daten wurden bereitgestellt."
assert "files" in req.result_data
def test_reject_dsr_request_body(self):
"""Test: RejectDSRRequest Model."""
from dsr_admin_api import RejectDSRRequest
req = RejectDSRRequest(
reason="Daten werden für Rechtsstreitigkeiten benötigt",
legal_basis="Art. 17(3)e"
)
assert req.reason == "Daten werden für Rechtsstreitigkeiten benötigt"
assert req.legal_basis == "Art. 17(3)e"
def test_send_communication_request_body(self):
"""Test: SendCommunicationRequest Model."""
from dsr_admin_api import SendCommunicationRequest
req = SendCommunicationRequest(
communication_type="dsr_processing_started",
template_version_id="uuid-123",
variables={"custom_field": "value"}
)
assert req.communication_type == "dsr_processing_started"
assert req.template_version_id == "uuid-123"
def test_update_exception_check_request_body(self):
"""Test: UpdateExceptionCheckRequest Model."""
from dsr_admin_api import UpdateExceptionCheckRequest
req = UpdateExceptionCheckRequest(
applies=True,
notes="Laufende Rechtsstreitigkeiten"
)
assert req.applies is True
assert req.notes == "Laufende Rechtsstreitigkeiten"
def test_create_template_version_request_body(self):
"""Test: CreateTemplateVersionRequest Model."""
from dsr_admin_api import CreateTemplateVersionRequest
req = CreateTemplateVersionRequest(
version="1.1.0",
language="de",
subject="Eingangsbestätigung",
body_html="<p>Inhalt</p>",
body_text="Inhalt"
)
assert req.version == "1.1.0"
assert req.language == "de"
assert req.subject == "Eingangsbestätigung"
def test_get_admin_token_from_header(self):
"""Test: Admin-Token aus Header extrahieren."""
from dsr_admin_api import get_admin_token
# Mit Bearer Token
token = get_admin_token("Bearer admin-jwt-token")
assert token == "admin-jwt-token"
def test_get_admin_token_fallback(self):
"""Test: Admin-Token Fallback für Entwicklung."""
from dsr_admin_api import get_admin_token
# Ohne Header - generiert Dev-Token
token = get_admin_token(None)
assert token is not None
assert len(token) > 0
class TestDSRRequestTypes:
"""Tests für DSR-Anfragetypen und DSGVO-Artikel."""
def test_access_request_art_15(self):
"""Test: Auskunftsrecht (Art. 15 DSGVO)."""
# 30 Tage Frist
expected_deadline_days = 30
assert expected_deadline_days == 30
def test_rectification_request_art_16(self):
"""Test: Berichtigungsrecht (Art. 16 DSGVO)."""
# 14 Tage empfohlen
expected_deadline_days = 14
assert expected_deadline_days == 14
def test_erasure_request_art_17(self):
"""Test: Löschungsrecht (Art. 17 DSGVO)."""
# 14 Tage empfohlen
expected_deadline_days = 14
assert expected_deadline_days == 14
def test_restriction_request_art_18(self):
"""Test: Einschränkungsrecht (Art. 18 DSGVO)."""
# 14 Tage empfohlen
expected_deadline_days = 14
assert expected_deadline_days == 14
def test_portability_request_art_20(self):
"""Test: Datenübertragbarkeit (Art. 20 DSGVO)."""
# 30 Tage Frist
expected_deadline_days = 30
assert expected_deadline_days == 30
class TestDSRStatusWorkflow:
"""Tests für den DSR Status-Workflow."""
def test_valid_status_values(self):
"""Test: Alle gültigen Status-Werte."""
valid_statuses = [
"intake",
"identity_verification",
"processing",
"completed",
"rejected",
"cancelled"
]
for status in valid_statuses:
assert status in valid_statuses
def test_status_transition_intake(self):
"""Test: Erlaubte Übergänge von 'intake'."""
allowed_from_intake = [
"identity_verification",
"processing",
"rejected",
"cancelled"
]
# Completed ist NICHT direkt von intake erlaubt
assert "completed" not in allowed_from_intake
def test_status_transition_processing(self):
"""Test: Erlaubte Übergänge von 'processing'."""
allowed_from_processing = [
"completed",
"rejected",
"cancelled"
]
# Zurück zu intake ist NICHT erlaubt
assert "intake" not in allowed_from_processing
def test_terminal_states(self):
"""Test: Endstatus ohne weitere Übergänge."""
terminal_states = ["completed", "rejected", "cancelled"]
for state in terminal_states:
# Von Endstatus keine Übergänge möglich
assert state in terminal_states
class TestDSRExceptionChecks:
"""Tests für Art. 17(3) Ausnahmeprüfungen."""
def test_exception_types_art_17_3(self):
"""Test: Alle Ausnahmen nach Art. 17(3) DSGVO."""
exceptions = {
"art_17_3_a": "Meinungs- und Informationsfreiheit",
"art_17_3_b": "Rechtliche Verpflichtung",
"art_17_3_c": "Öffentliches Interesse im Gesundheitsbereich",
"art_17_3_d": "Archivzwecke, wissenschaftliche/historische Forschung",
"art_17_3_e": "Geltendmachung von Rechtsansprüchen"
}
assert len(exceptions) == 5
for code, description in exceptions.items():
assert code.startswith("art_17_3_")
assert len(description) > 0
def test_rejection_legal_bases(self):
"""Test: Rechtsgrundlagen für Ablehnung."""
legal_bases = [
"Art. 17(3)a",
"Art. 17(3)b",
"Art. 17(3)c",
"Art. 17(3)d",
"Art. 17(3)e",
"Art. 12(5)" # Offensichtlich unbegründet/exzessiv
]
assert len(legal_bases) == 6
assert "Art. 12(5)" in legal_bases
class TestDSRTemplates:
"""Tests für DSR-Vorlagen."""
def test_template_types(self):
"""Test: Alle erwarteten Vorlagen-Typen."""
expected_templates = [
"dsr_receipt_access",
"dsr_receipt_rectification",
"dsr_receipt_erasure",
"dsr_receipt_restriction",
"dsr_receipt_portability",
"dsr_identity_request",
"dsr_processing_started",
"dsr_processing_update",
"dsr_clarification_request",
"dsr_completed_access",
"dsr_completed_rectification",
"dsr_completed_erasure",
"dsr_completed_restriction",
"dsr_completed_portability",
"dsr_restriction_lifted",
"dsr_rejected_identity",
"dsr_rejected_exception",
"dsr_rejected_unfounded",
"dsr_deadline_warning"
]
assert len(expected_templates) == 19
def test_template_variables(self):
"""Test: Standard Template-Variablen."""
variables = [
"{{requester_name}}",
"{{requester_email}}",
"{{request_number}}",
"{{request_type_de}}",
"{{request_date}}",
"{{deadline_date}}",
"{{company_name}}",
"{{dpo_name}}",
"{{dpo_email}}",
"{{portal_url}}"
]
for var in variables:
assert var.startswith("{{")
assert var.endswith("}}")