diff --git a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx index 7698564..9d6721d 100644 --- a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx +++ b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx @@ -13,7 +13,21 @@ import { DSRCommunication, DSRVerifyIdentityRequest } from '@/lib/sdk/dsr/types' -import { fetchSDKDSR, updateSDKDSRStatus } from '@/lib/sdk/dsr/api' +import { + fetchSDKDSR, + updateSDKDSRStatus, + verifyDSRIdentity, + assignDSR, + extendDSRDeadline, + completeDSR, + rejectDSR, + fetchDSRCommunications, + sendDSRCommunication, + fetchDSRExceptionChecks, + initDSRExceptionChecks, + updateDSRExceptionCheck, + fetchDSRHistory, +} from '@/lib/sdk/dsr/api' import { DSRWorkflowStepper, DSRIdentityModal, @@ -22,12 +36,6 @@ import { DSRDataExportComponent } from '@/components/sdk/dsr' -// ============================================================================= -// MOCK COMMUNICATIONS -// ============================================================================= - -// TODO: Backend fehlt — Communications API noch nicht implementiert - // ============================================================================= // COMPONENTS // ============================================================================= @@ -155,56 +163,38 @@ function ActionButtons({ ) } -function AuditLog({ request }: { request: DSRRequest }) { - type AuditEvent = { action: string; timestamp: string; user: string } - - const events: AuditEvent[] = [ - { action: 'Erstellt', timestamp: request.createdAt, user: request.createdBy } - ] - - if (request.assignment.assignedAt) { - events.push({ - action: `Zugewiesen an ${request.assignment.assignedTo}`, - timestamp: request.assignment.assignedAt, - user: request.assignment.assignedBy || 'System' - }) - } - - if (request.identityVerification.verifiedAt) { - events.push({ - action: 'Identitaet verifiziert', - timestamp: request.identityVerification.verifiedAt, - user: request.identityVerification.verifiedBy || 'System' - }) - } - - if (request.completedAt) { - events.push({ - action: request.status === 'rejected' ? 'Abgelehnt' : 'Abgeschlossen', - timestamp: request.completedAt, - user: request.updatedBy || 'System' - }) - } - +function AuditLog({ history }: { history: any[] }) { return (

Aktivitaeten

- {events.map((event, idx) => ( -
+ {history.length === 0 && ( +
Keine Eintraege
+ )} + {history.map((entry, idx) => ( +
-
{event.action}
+
+ {entry.previous_status + ? `${entry.previous_status} → ${entry.new_status}` + : entry.new_status + } + {entry.comment && `: ${entry.comment}`} +
- {new Date(event.timestamp).toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} + {entry.created_at + ? new Date(entry.created_at).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : '' + } {' - '} - {event.user} + {entry.changed_by || 'System'}
@@ -225,10 +215,17 @@ export default function DSRDetailPage() { const [request, setRequest] = useState(null) const [communications, setCommunications] = useState([]) + const [history, setHistory] = useState([]) + const [exceptionChecks, setExceptionChecks] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showIdentityModal, setShowIdentityModal] = useState(false) const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details') + const reloadRequest = async () => { + const found = await fetchSDKDSR(requestId) + if (found) setRequest(found) + } + // Load data from SDK backend useEffect(() => { const loadData = async () => { @@ -237,8 +234,40 @@ export default function DSRDetailPage() { const found = await fetchSDKDSR(requestId) if (found) { setRequest(found) - // TODO: Backend fehlt — Communications API noch nicht implementiert - setCommunications([]) + // Load communications, history, and exception checks in parallel + const [comms, hist] = await Promise.all([ + fetchDSRCommunications(requestId).catch(() => []), + fetchDSRHistory(requestId).catch(() => []), + ]) + setCommunications(comms.map((c: any) => ({ + id: c.id, + dsrId: c.dsr_id, + type: c.communication_type, + channel: c.channel, + subject: c.subject, + content: c.content, + createdAt: c.created_at, + createdBy: c.created_by, + sentAt: c.sent_at, + sentBy: c.sent_by, + }))) + setHistory(hist) + + // Load exception checks for erasure requests + if (found.type === 'erasure') { + try { + const checks = await fetchDSRExceptionChecks(requestId) + if (checks.length === 0) { + // Auto-initialize if none exist + const initialized = await initDSRExceptionChecks(requestId) + setExceptionChecks(initialized) + } else { + setExceptionChecks(checks) + } + } catch { + setExceptionChecks([]) + } + } } } catch (error) { console.error('Failed to load DSR:', error) @@ -252,46 +281,109 @@ export default function DSRDetailPage() { const handleVerifyIdentity = async (verification: DSRVerifyIdentityRequest) => { if (!request) return try { - await updateSDKDSRStatus(request.id, 'verified') - setRequest({ - ...request, - identityVerification: { - verified: true, - method: verification.method, - verifiedAt: new Date().toISOString(), - verifiedBy: 'Current User', - notes: verification.notes - }, - status: request.status === 'identity_verification' ? 'processing' : request.status + const updated = await verifyDSRIdentity(request.id, { + method: verification.method, + notes: verification.notes, + document_ref: verification.documentRef, }) + setRequest(updated) + // Reload history + fetchDSRHistory(requestId).then(setHistory).catch(() => {}) } catch (err) { console.error('Failed to verify identity:', err) - // Still update locally as fallback - setRequest({ - ...request, - identityVerification: { - verified: true, - method: verification.method, - verifiedAt: new Date().toISOString(), - verifiedBy: 'Current User', - notes: verification.notes - }, - status: request.status === 'identity_verification' ? 'processing' : request.status - }) + } + } + + const handleAssign = async () => { + if (!request) return + const assignee = prompt('Zuweisen an (Name/ID):') + if (!assignee) return + try { + const updated = await assignDSR(request.id, assignee) + setRequest(updated) + fetchDSRHistory(requestId).then(setHistory).catch(() => {}) + } catch (err) { + console.error('Failed to assign:', err) + } + } + + const handleExtendDeadline = async () => { + if (!request) return + const reason = prompt('Grund fuer die Fristverlaengerung:') + if (!reason) return + const daysStr = prompt('Um wie viele Tage verlaengern? (Standard: 60)', '60') + const days = parseInt(daysStr || '60', 10) || 60 + try { + const updated = await extendDSRDeadline(request.id, reason, days) + setRequest(updated) + fetchDSRHistory(requestId).then(setHistory).catch(() => {}) + } catch (err) { + console.error('Failed to extend deadline:', err) + } + } + + const handleComplete = async () => { + if (!request) return + const summary = prompt('Zusammenfassung der Bearbeitung:') + if (summary === null) return + try { + const updated = await completeDSR(request.id, summary || undefined) + setRequest(updated) + fetchDSRHistory(requestId).then(setHistory).catch(() => {}) + } catch (err) { + console.error('Failed to complete:', err) + } + } + + const handleReject = async () => { + if (!request) return + const reason = prompt('Ablehnungsgrund:') + if (!reason) return + const legalBasis = prompt('Rechtsgrundlage (optional):') + try { + const updated = await rejectDSR(request.id, reason, legalBasis || undefined) + setRequest(updated) + fetchDSRHistory(requestId).then(setHistory).catch(() => {}) + } catch (err) { + console.error('Failed to reject:', err) } } const handleSendCommunication = async (message: any) => { - const newComm: DSRCommunication = { - id: `comm-${Date.now()}`, - dsrId: requestId, - ...message, - createdAt: new Date().toISOString(), - createdBy: 'Current User', - sentAt: message.type === 'outgoing' ? new Date().toISOString() : undefined, - sentBy: message.type === 'outgoing' ? 'Current User' : undefined + if (!request) return + try { + const result = await sendDSRCommunication(requestId, { + communication_type: message.type || 'outgoing', + channel: message.channel || 'email', + subject: message.subject, + content: message.content, + }) + // Map backend response to frontend format + const newComm: DSRCommunication = { + id: result.id, + dsrId: result.dsr_id, + type: result.communication_type, + channel: result.channel, + subject: result.subject, + content: result.content, + createdAt: result.created_at, + createdBy: result.created_by || 'Current User', + sentAt: result.sent_at, + sentBy: result.sent_by, + } + setCommunications(prev => [newComm, ...prev]) + } catch (err) { + console.error('Failed to send communication:', err) + } + } + + const handleExceptionCheckChange = async (checkId: string, applies: boolean, notes?: string) => { + try { + const updated = await updateDSRExceptionCheck(requestId, checkId, { applies, notes }) + setExceptionChecks(prev => prev.map(c => c.id === checkId ? updated : c)) + } catch (err) { + console.error('Failed to update exception check:', err) } - setCommunications(prev => [newComm, ...prev]) } if (isLoading) { @@ -528,10 +620,48 @@ export default function DSRDetailPage() {
{/* Art. 17 - Erasure */} {request.type === 'erasure' && ( - setRequest({ ...request, erasureChecklist: checklist })} - /> +
+ setRequest({ ...request, erasureChecklist: checklist })} + /> + + {/* Art. 17(3) Exception Checks from Backend */} + {exceptionChecks.length > 0 && ( +
+

Art. 17(3) Ausnahmepruefung

+

+ Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift. +

+
+ {exceptionChecks.map((check) => ( +
+
+ handleExceptionCheckChange(check.id, e.target.checked, check.notes)} + className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" + /> +
+
+ {check.article}: {check.label} +
+
{check.description}
+ {check.checked_by && ( +
+ Geprueft von {check.checked_by} am{' '} + {check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'} +
+ )} +
+
+
+ ))} +
+
+ )} +
)} {/* Art. 15/20 - Data Export */} @@ -697,16 +827,16 @@ export default function DSRDetailPage() { setShowIdentityModal(true)} - onExtendDeadline={() => alert('Fristverlaengerung - Coming soon')} - onComplete={() => alert('Abschliessen - Coming soon')} - onReject={() => alert('Ablehnen - Coming soon')} - onAssign={() => alert('Zuweisen - Coming soon')} + onExtendDeadline={handleExtendDeadline} + onComplete={handleComplete} + onReject={handleReject} + onAssign={handleAssign} />
{/* Audit Log Card */}
- +
diff --git a/admin-compliance/app/sdk/dsr/page.tsx b/admin-compliance/app/sdk/dsr/page.tsx index 7d7ee6d..7d880b2 100644 --- a/admin-compliance/app/sdk/dsr/page.tsx +++ b/admin-compliance/app/sdk/dsr/page.tsx @@ -808,15 +808,31 @@ export default function DSRPage() { explanation={stepInfo.explanation} tips={stepInfo.tips} > - +
+ + +
{/* Tab Navigation */} diff --git a/admin-compliance/lib/sdk/dsr/api.ts b/admin-compliance/lib/sdk/dsr/api.ts index a351311..cc58d46 100644 --- a/admin-compliance/lib/sdk/dsr/api.ts +++ b/admin-compliance/lib/sdk/dsr/api.ts @@ -245,6 +245,171 @@ export async function updateSDKDSRStatus(id: string, status: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Assign DSR to a user + */ +export async function assignDSR(id: string, assigneeId: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ assignee_id: assigneeId }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Extend DSR deadline (Art. 12 Abs. 3 DSGVO) + */ +export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ reason, days }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Complete a DSR + */ +export async function completeDSR(id: string, summary?: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ summary }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +/** + * Reject a DSR with legal basis + */ +export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ reason, legal_basis: legalBasis }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + +// ============================================================================= +// COMMUNICATIONS +// ============================================================================= + +/** + * Fetch communications for a DSR + */ +export async function fetchDSRCommunications(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Send a communication for a DSR + */ +export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, { + method: 'POST', + headers: getSdkHeaders(), + body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +// ============================================================================= +// EXCEPTION CHECKS (Art. 17) +// ============================================================================= + +/** + * Fetch exception checks for an erasure DSR + */ +export async function fetchDSRExceptionChecks(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Initialize Art. 17(3) exception checks for an erasure DSR + */ +export async function initDSRExceptionChecks(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, { + method: 'POST', + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Update a single exception check + */ +export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, { + method: 'PUT', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +// ============================================================================= +// HISTORY +// ============================================================================= + +/** + * Fetch status change history for a DSR + */ +export async function fetchDSRHistory(id: string): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, { + headers: getSdkHeaders(), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() +} + +/** + * Update DSR fields (priority, notes, etc.) + */ +export async function updateDSR(id: string, data: Record): Promise { + const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { + method: 'PUT', + headers: getSdkHeaders(), + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return transformBackendDSR(await res.json()) +} + // ============================================================================= // MOCK DATA FUNCTIONS (kept as fallback) // ============================================================================= diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go index a1e55d4..14efdb3 100644 --- a/ai-compliance-sdk/cmd/server/main.go +++ b/ai-compliance-sdk/cmd/server/main.go @@ -273,6 +273,8 @@ func main() { } // DSR - Data Subject Requests / Betroffenenrechte (Art. 15-22) + // DEPRECATED: DSR is now managed by backend-compliance (Python). + // Use: GET/POST/PUT /api/compliance/dsr/* on backend-compliance:8002 dsr := dsgvoRoutes.Group("/dsr") { dsr.GET("", dsgvoHandlers.ListDSRs) @@ -304,7 +306,7 @@ func main() { { exports.GET("/vvt", dsgvoHandlers.ExportVVT) // DEPRECATED: use backend-compliance /vvt/export?format=csv exports.GET("/tom", dsgvoHandlers.ExportTOM) // DEPRECATED: use backend-compliance /tom/export?format=csv - exports.GET("/dsr", dsgvoHandlers.ExportDSR) + exports.GET("/dsr", dsgvoHandlers.ExportDSR) // DEPRECATED: use backend-compliance /dsr/export?format=csv exports.GET("/retention", dsgvoHandlers.ExportRetentionPolicies) } } diff --git a/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go b/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go index f44c069..180cba3 100644 --- a/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go +++ b/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go @@ -220,9 +220,12 @@ func (h *DSGVOHandlers) CreateTOM(c *gin.Context) { // ============================================================================ // DSR - Data Subject Requests +// DEPRECATED: DSR is now managed by backend-compliance (Python/FastAPI). +// Use: /api/compliance/dsr/* endpoints on backend-compliance:8002 // ============================================================================ // ListDSRs returns all DSRs for a tenant +// DEPRECATED: Use backend-compliance GET /api/compliance/dsr func (h *DSGVOHandlers) ListDSRs(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { @@ -243,6 +246,7 @@ func (h *DSGVOHandlers) ListDSRs(c *gin.Context) { } // GetDSR returns a DSR by ID +// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/{id} func (h *DSGVOHandlers) GetDSR(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -264,6 +268,7 @@ func (h *DSGVOHandlers) GetDSR(c *gin.Context) { } // CreateDSR creates a new DSR +// DEPRECATED: Use backend-compliance POST /api/compliance/dsr func (h *DSGVOHandlers) CreateDSR(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) @@ -293,6 +298,7 @@ func (h *DSGVOHandlers) CreateDSR(c *gin.Context) { } // UpdateDSR updates a DSR +// DEPRECATED: Use backend-compliance PUT /api/compliance/dsr/{id} func (h *DSGVOHandlers) UpdateDSR(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -612,6 +618,7 @@ func (h *DSGVOHandlers) ExportTOM(c *gin.Context) { } // ExportDSR exports DSR overview as CSV/JSON +// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/export?format=csv|json func (h *DSGVOHandlers) ExportDSR(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { diff --git a/backend-compliance/compliance/api/dsr_routes.py b/backend-compliance/compliance/api/dsr_routes.py index 8b05d13..0a4db34 100644 --- a/backend-compliance/compliance/api/dsr_routes.py +++ b/backend-compliance/compliance/api/dsr_routes.py @@ -4,11 +4,14 @@ DSR (Data Subject Request) Routes — Betroffenenanfragen nach DSGVO Art. 15-21. Native Python/FastAPI Implementierung, ersetzt Go consent-service Proxy. """ +import io +import csv import uuid from datetime import datetime, timedelta from typing import Optional, List, Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query, Header +from fastapi.responses import StreamingResponse from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy import text, func, and_, or_, cast, String @@ -438,6 +441,61 @@ async def get_dsr_stats( } +# ============================================================================= +# Export +# ============================================================================= + +@router.get("/export") +async def export_dsrs( + format: str = Query("csv", pattern="^(csv|json)$"), + tenant_id: str = Depends(_get_tenant), + db: Session = Depends(get_db), +): + """Exportiert alle DSRs als CSV oder JSON.""" + tid = uuid.UUID(tenant_id) + dsrs = db.query(DSRRequestDB).filter( + DSRRequestDB.tenant_id == tid, + ).order_by(DSRRequestDB.created_at.desc()).all() + + if format == "json": + return { + "exported_at": datetime.utcnow().isoformat(), + "total": len(dsrs), + "requests": [_dsr_to_dict(d) for d in dsrs], + } + + # CSV export (semicolon-separated, matching Go format + extended fields) + output = io.StringIO() + writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL) + writer.writerow([ + "ID", "Referenznummer", "Typ", "Name", "E-Mail", "Status", + "Prioritaet", "Eingegangen", "Frist", "Abgeschlossen", "Quelle", "Zugewiesen", + ]) + + for dsr in dsrs: + writer.writerow([ + str(dsr.id), + dsr.request_number or "", + dsr.request_type or "", + dsr.requester_name or "", + dsr.requester_email or "", + dsr.status or "", + dsr.priority or "", + dsr.received_at.strftime("%Y-%m-%d") if dsr.received_at else "", + dsr.deadline_at.strftime("%Y-%m-%d") if dsr.deadline_at else "", + dsr.completed_at.strftime("%Y-%m-%d") if dsr.completed_at else "", + dsr.source or "", + dsr.assigned_to or "", + ]) + + output.seek(0) + return StreamingResponse( + output, + media_type="text/csv; charset=utf-8", + headers={"Content-Disposition": "attachment; filename=dsr_export.csv"}, + ) + + # ============================================================================= # Deadline Processing (MUST be before /{dsr_id} to avoid path conflicts) # ============================================================================= diff --git a/backend-compliance/tests/test_dsr_routes.py b/backend-compliance/tests/test_dsr_routes.py index 11540fc..ff91e9d 100644 --- a/backend-compliance/tests/test_dsr_routes.py +++ b/backend-compliance/tests/test_dsr_routes.py @@ -697,3 +697,49 @@ class TestDSRTemplates: 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