"""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