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
- 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>
385 lines
13 KiB
Python
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
|