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>
424 lines
14 KiB
Python
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("}}")
|