Files
breakpilot-compliance/backend-compliance/tests/test_dsfa_routes.py
Benjamin Admin a694b9d9ea
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
feat: DSFA Modul — Backend, Proxy, Frontend-Migration, Tests + Mock-Daten entfernt
- Migration 024: compliance_dsfas + compliance_dsfa_audit_log Tabellen
- dsfa_routes.py: CRUD + stats + audit-log + PATCH status Endpoints
- Proxy: /api/sdk/v1/dsfa/[[...path]] → backend-compliance:8002/api/v1/dsfa
- dsfa/page.tsx: mockDSFAs entfernt → echte API (loadDSFAs, handleCreateDSFA, handleStatusChange, handleDeleteDSFA)
- GeneratorWizard: kontrollierte Inputs + onSubmit-Handler
- reporting/page.tsx: getMockReport() Fallback entfernt → Fehlerstate
- dsr/[requestId]/page.tsx: mockCommunications entfernt → leeres Array (TODO: Backend fehlt)
- 52 neue Tests (680 gesamt, alle grün)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:41:05 +01:00

385 lines
13 KiB
Python

"""Tests for DSFA routes and schemas (dsfa_routes.py)."""
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from compliance.api.dsfa_routes import (
DSFACreate,
DSFAUpdate,
DSFAStatusUpdate,
_dsfa_to_response,
_get_tenant_id,
DEFAULT_TENANT_ID,
VALID_STATUSES,
VALID_RISK_LEVELS,
)
# =============================================================================
# Schema Tests — DSFACreate
# =============================================================================
class TestDSFACreate:
def test_minimal_valid(self):
req = DSFACreate(title="DSFA - Mitarbeiter-Monitoring")
assert req.title == "DSFA - Mitarbeiter-Monitoring"
assert req.status == "draft"
assert req.risk_level == "low"
assert req.description == ""
assert req.processing_activity == ""
assert req.data_categories == []
assert req.recipients == []
assert req.measures == []
assert req.created_by == "system"
def test_full_values(self):
req = DSFACreate(
title="DSFA - Video-Ueberwachung",
description="Videoueberwachung im Buero",
status="in-review",
risk_level="high",
processing_activity="Videoueberwachung zu Sicherheitszwecken",
data_categories=["Bilddaten", "Bewegungsdaten"],
recipients=["Sicherheitsdienst"],
measures=["Loeschfristen", "Hinweisschilder"],
created_by="admin",
)
assert req.title == "DSFA - Video-Ueberwachung"
assert req.status == "in-review"
assert req.risk_level == "high"
assert req.data_categories == ["Bilddaten", "Bewegungsdaten"]
assert req.recipients == ["Sicherheitsdienst"]
assert req.measures == ["Loeschfristen", "Hinweisschilder"]
assert req.created_by == "admin"
def test_draft_is_default_status(self):
req = DSFACreate(title="Test")
assert req.status == "draft"
def test_low_is_default_risk_level(self):
req = DSFACreate(title="Test")
assert req.risk_level == "low"
def test_empty_arrays_default(self):
req = DSFACreate(title="Test")
assert isinstance(req.data_categories, list)
assert isinstance(req.recipients, list)
assert isinstance(req.measures, list)
assert len(req.data_categories) == 0
def test_serialization_model_dump(self):
req = DSFACreate(title="Test", risk_level="critical")
data = req.model_dump()
assert data["title"] == "Test"
assert data["risk_level"] == "critical"
assert "status" in data
assert "data_categories" in data
# =============================================================================
# Schema Tests — DSFAUpdate
# =============================================================================
class TestDSFAUpdate:
def test_all_optional(self):
req = DSFAUpdate()
assert req.title is None
assert req.description is None
assert req.status is None
assert req.risk_level is None
assert req.processing_activity is None
assert req.data_categories is None
assert req.recipients is None
assert req.measures is None
assert req.approved_by is None
def test_partial_update_title_only(self):
req = DSFAUpdate(title="Neuer Titel")
data = req.model_dump(exclude_none=True)
assert data == {"title": "Neuer Titel"}
def test_partial_update_status_and_risk(self):
req = DSFAUpdate(status="approved", risk_level="medium")
data = req.model_dump(exclude_none=True)
assert data["status"] == "approved"
assert data["risk_level"] == "medium"
assert "title" not in data
def test_update_arrays(self):
req = DSFAUpdate(data_categories=["Kontaktdaten"], measures=["Verschluesselung"])
assert req.data_categories == ["Kontaktdaten"]
assert req.measures == ["Verschluesselung"]
def test_exclude_none_removes_unset(self):
req = DSFAUpdate(approved_by="DSB Mueller")
data = req.model_dump(exclude_none=True)
assert data == {"approved_by": "DSB Mueller"}
# =============================================================================
# Schema Tests — DSFAStatusUpdate
# =============================================================================
class TestDSFAStatusUpdate:
def test_status_only(self):
req = DSFAStatusUpdate(status="approved")
assert req.status == "approved"
assert req.approved_by is None
def test_status_with_approved_by(self):
req = DSFAStatusUpdate(status="approved", approved_by="DSB Mueller")
assert req.status == "approved"
assert req.approved_by == "DSB Mueller"
def test_in_review_status(self):
req = DSFAStatusUpdate(status="in-review")
assert req.status == "in-review"
def test_needs_update_status(self):
req = DSFAStatusUpdate(status="needs-update")
assert req.status == "needs-update"
# =============================================================================
# Helper Tests — _get_tenant_id
# =============================================================================
class TestGetTenantId:
def test_none_returns_default(self):
assert _get_tenant_id(None) == DEFAULT_TENANT_ID
def test_empty_string_returns_empty(self):
# Empty string is falsy → returns default
assert _get_tenant_id("") == DEFAULT_TENANT_ID
def test_custom_tenant_id(self):
assert _get_tenant_id("my-tenant") == "my-tenant"
def test_default_constant_value(self):
assert DEFAULT_TENANT_ID == "default"
# =============================================================================
# Helper Tests — _dsfa_to_response
# =============================================================================
class TestDsfaToResponse:
def _make_row(self, **overrides):
defaults = {
"id": "abc123",
"tenant_id": "default",
"title": "Test DSFA",
"description": "Testbeschreibung",
"status": "draft",
"risk_level": "low",
"processing_activity": "Test-Verarbeitung",
"data_categories": ["Kontaktdaten"],
"recipients": ["HR"],
"measures": ["Verschluesselung"],
"approved_by": None,
"approved_at": None,
"created_by": "system",
"created_at": datetime(2026, 1, 1, 12, 0, 0),
"updated_at": datetime(2026, 1, 2, 12, 0, 0),
}
defaults.update(overrides)
row = MagicMock()
row.__getitem__ = lambda self, key: defaults[key]
return row
def test_basic_fields(self):
row = self._make_row()
result = _dsfa_to_response(row)
assert result["id"] == "abc123"
assert result["title"] == "Test DSFA"
assert result["status"] == "draft"
assert result["risk_level"] == "low"
def test_dates_as_iso_strings(self):
row = self._make_row()
result = _dsfa_to_response(row)
assert result["created_at"] == "2026-01-01T12:00:00"
assert result["updated_at"] == "2026-01-02T12:00:00"
def test_approved_at_none_when_not_set(self):
row = self._make_row(approved_at=None)
result = _dsfa_to_response(row)
assert result["approved_at"] is None
def test_approved_at_iso_when_set(self):
row = self._make_row(approved_at=datetime(2026, 3, 1, 10, 0, 0))
result = _dsfa_to_response(row)
assert result["approved_at"] == "2026-03-01T10:00:00"
def test_null_description_becomes_empty_string(self):
row = self._make_row(description=None)
result = _dsfa_to_response(row)
assert result["description"] == ""
def test_json_string_data_categories_parsed(self):
import json
row = self._make_row(data_categories=json.dumps(["Kontaktdaten", "Finanzdaten"]))
result = _dsfa_to_response(row)
assert result["data_categories"] == ["Kontaktdaten", "Finanzdaten"]
def test_null_arrays_become_empty_lists(self):
row = self._make_row(data_categories=None, recipients=None, measures=None)
result = _dsfa_to_response(row)
assert result["data_categories"] == []
assert result["recipients"] == []
assert result["measures"] == []
def test_null_status_defaults_to_draft(self):
row = self._make_row(status=None)
result = _dsfa_to_response(row)
assert result["status"] == "draft"
def test_null_risk_level_defaults_to_low(self):
row = self._make_row(risk_level=None)
result = _dsfa_to_response(row)
assert result["risk_level"] == "low"
# =============================================================================
# Valid Status Values
# =============================================================================
class TestValidStatusValues:
def test_draft_is_valid(self):
assert "draft" in VALID_STATUSES
def test_in_review_is_valid(self):
assert "in-review" in VALID_STATUSES
def test_approved_is_valid(self):
assert "approved" in VALID_STATUSES
def test_needs_update_is_valid(self):
assert "needs-update" in VALID_STATUSES
def test_invalid_status_not_in_set(self):
assert "invalid_status" not in VALID_STATUSES
def test_all_four_statuses_covered(self):
assert len(VALID_STATUSES) == 4
# =============================================================================
# Valid Risk Levels
# =============================================================================
class TestValidRiskLevels:
def test_low_is_valid(self):
assert "low" in VALID_RISK_LEVELS
def test_medium_is_valid(self):
assert "medium" in VALID_RISK_LEVELS
def test_high_is_valid(self):
assert "high" in VALID_RISK_LEVELS
def test_critical_is_valid(self):
assert "critical" in VALID_RISK_LEVELS
def test_invalid_risk_not_in_set(self):
assert "extreme" not in VALID_RISK_LEVELS
def test_all_four_levels_covered(self):
assert len(VALID_RISK_LEVELS) == 4
# =============================================================================
# Router Config
# =============================================================================
class TestDSFARouterConfig:
def test_router_prefix(self):
from compliance.api.dsfa_routes import router
assert router.prefix == "/v1/dsfa"
def test_router_has_tags(self):
from compliance.api.dsfa_routes import router
assert "compliance-dsfa" in router.tags
def test_router_registered_in_init(self):
from compliance.api import dsfa_router
assert dsfa_router is not None
# =============================================================================
# Stats Response Structure
# =============================================================================
class TestDSFAStatsResponse:
def test_stats_keys_present(self):
"""Stats endpoint must return these keys."""
expected_keys = {
"total", "by_status", "by_risk_level",
"draft_count", "in_review_count", "approved_count", "needs_update_count"
}
# Verify by constructing the expected dict shape
stats = {
"total": 0,
"by_status": {},
"by_risk_level": {},
"draft_count": 0,
"in_review_count": 0,
"approved_count": 0,
"needs_update_count": 0,
}
assert set(stats.keys()) == expected_keys
def test_stats_total_is_int(self):
stats = {"total": 5}
assert isinstance(stats["total"], int)
def test_stats_by_status_is_dict(self):
by_status = {"draft": 2, "approved": 1}
assert isinstance(by_status, dict)
def test_stats_counts_are_integers(self):
counts = {"draft_count": 2, "in_review_count": 1, "approved_count": 0}
assert all(isinstance(v, int) for v in counts.values())
def test_stats_zero_total_when_no_dsfas(self):
stats = {"total": 0, "draft_count": 0, "in_review_count": 0, "approved_count": 0}
assert stats["total"] == 0
# =============================================================================
# Audit Log Entry Structure
# =============================================================================
class TestAuditLogEntry:
def test_audit_log_entry_keys(self):
entry = {
"id": "uuid-1",
"tenant_id": "default",
"dsfa_id": "uuid-2",
"action": "CREATE",
"changed_by": "system",
"old_values": None,
"new_values": {"title": "Test"},
"created_at": "2026-01-01T12:00:00",
}
assert "id" in entry
assert "action" in entry
assert "dsfa_id" in entry
assert "created_at" in entry
def test_audit_action_values(self):
valid_actions = {"CREATE", "UPDATE", "DELETE", "STATUS_CHANGE"}
assert "CREATE" in valid_actions
assert "DELETE" in valid_actions
assert "STATUS_CHANGE" in valid_actions
def test_audit_dsfa_id_can_be_none(self):
entry = {"dsfa_id": None}
assert entry["dsfa_id"] is None
def test_audit_old_values_can_be_none(self):
entry = {"old_values": None, "new_values": {"title": "Test"}}
assert entry["old_values"] is None
assert entry["new_values"] is not None