Review-Daten (last_reviewed_at, next_review_at), created_by, DSFA-Link, CSV-Export mit Semikolon-Trennung, overdue_review_count in Stats. Go-VVT-Handler als DEPRECATED markiert. 32 Tests bestanden. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
434 lines
15 KiB
Python
434 lines
15 KiB
Python
"""Tests for VVT routes and schemas (vvt_routes.py, vvt_models.py)."""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
from datetime import datetime, date, timedelta, timezone
|
|
import uuid
|
|
|
|
from compliance.api.schemas import (
|
|
VVTActivityCreate,
|
|
VVTActivityUpdate,
|
|
VVTOrganizationUpdate,
|
|
VVTStatsResponse,
|
|
)
|
|
from compliance.api.vvt_routes import _activity_to_response, _log_audit
|
|
from compliance.db.vvt_models import VVTActivityDB, VVTOrganizationDB, VVTAuditLogDB
|
|
|
|
|
|
# =============================================================================
|
|
# Schema Tests
|
|
# =============================================================================
|
|
|
|
class TestVVTActivityCreate:
|
|
def test_default_values(self):
|
|
req = VVTActivityCreate(vvt_id="VVT-001", name="Test Verarbeitung")
|
|
assert req.vvt_id == "VVT-001"
|
|
assert req.name == "Test Verarbeitung"
|
|
assert req.status == "DRAFT"
|
|
assert req.protection_level == "MEDIUM"
|
|
assert req.dpia_required is False
|
|
assert req.purposes == []
|
|
assert req.legal_bases == []
|
|
|
|
def test_full_values(self):
|
|
req = VVTActivityCreate(
|
|
vvt_id="VVT-002",
|
|
name="Gehaltsabrechnung",
|
|
description="Verarbeitung von Gehaltsabrechnungsdaten",
|
|
purposes=["Vertragserfuellung"],
|
|
legal_bases=["Art. 6 Abs. 1b DSGVO"],
|
|
data_subject_categories=["Mitarbeiter"],
|
|
personal_data_categories=["Bankdaten", "Steuer-ID"],
|
|
status="APPROVED",
|
|
dpia_required=False,
|
|
)
|
|
assert req.vvt_id == "VVT-002"
|
|
assert req.status == "APPROVED"
|
|
assert len(req.purposes) == 1
|
|
assert len(req.personal_data_categories) == 2
|
|
|
|
def test_serialization(self):
|
|
req = VVTActivityCreate(vvt_id="VVT-003", name="Test")
|
|
data = req.model_dump()
|
|
assert data["vvt_id"] == "VVT-003"
|
|
assert isinstance(data["purposes"], list)
|
|
assert isinstance(data["retention_period"], dict)
|
|
|
|
|
|
class TestVVTActivityUpdate:
|
|
def test_partial_update(self):
|
|
req = VVTActivityUpdate(status="APPROVED")
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data == {"status": "APPROVED"}
|
|
|
|
def test_empty_update(self):
|
|
req = VVTActivityUpdate()
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data == {}
|
|
|
|
def test_multi_field_update(self):
|
|
req = VVTActivityUpdate(
|
|
name="Updated Name",
|
|
dpia_required=True,
|
|
protection_level="HIGH",
|
|
)
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data["name"] == "Updated Name"
|
|
assert data["dpia_required"] is True
|
|
assert data["protection_level"] == "HIGH"
|
|
|
|
|
|
class TestVVTOrganizationUpdate:
|
|
def test_defaults(self):
|
|
req = VVTOrganizationUpdate()
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data == {}
|
|
|
|
def test_partial_update(self):
|
|
req = VVTOrganizationUpdate(
|
|
organization_name="BreakPilot GmbH",
|
|
dpo_name="Max Mustermann",
|
|
)
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data["organization_name"] == "BreakPilot GmbH"
|
|
assert data["dpo_name"] == "Max Mustermann"
|
|
|
|
|
|
class TestVVTStatsResponse:
|
|
def test_stats_response(self):
|
|
stats = VVTStatsResponse(
|
|
total=5,
|
|
by_status={"DRAFT": 3, "APPROVED": 2},
|
|
by_business_function={"HR": 2, "IT": 3},
|
|
dpia_required_count=1,
|
|
third_country_count=0,
|
|
draft_count=3,
|
|
approved_count=2,
|
|
overdue_review_count=1,
|
|
)
|
|
assert stats.total == 5
|
|
assert stats.by_status["DRAFT"] == 3
|
|
assert stats.dpia_required_count == 1
|
|
assert stats.overdue_review_count == 1
|
|
|
|
def test_stats_overdue_default_zero(self):
|
|
stats = VVTStatsResponse(
|
|
total=0,
|
|
by_status={},
|
|
by_business_function={},
|
|
dpia_required_count=0,
|
|
third_country_count=0,
|
|
draft_count=0,
|
|
approved_count=0,
|
|
)
|
|
assert stats.overdue_review_count == 0
|
|
|
|
|
|
# =============================================================================
|
|
# DB Model Tests
|
|
# =============================================================================
|
|
|
|
class TestVVTModels:
|
|
def test_activity_defaults(self):
|
|
act = VVTActivityDB()
|
|
assert act.status is None or act.status == 'DRAFT'
|
|
assert act.dpia_required is False or act.dpia_required is None
|
|
|
|
def test_activity_repr(self):
|
|
act = VVTActivityDB()
|
|
act.vvt_id = "VVT-001"
|
|
act.name = "Test"
|
|
assert "VVT-001" in repr(act)
|
|
|
|
def test_organization_repr(self):
|
|
org = VVTOrganizationDB()
|
|
org.organization_name = "Test GmbH"
|
|
assert "Test GmbH" in repr(org)
|
|
|
|
def test_audit_log_repr(self):
|
|
log = VVTAuditLogDB()
|
|
log.action = "CREATE"
|
|
log.entity_type = "activity"
|
|
assert "CREATE" in repr(log)
|
|
|
|
|
|
# =============================================================================
|
|
# Helper Function Tests
|
|
# =============================================================================
|
|
|
|
class TestActivityToResponse:
|
|
def _make_activity(self, **kwargs) -> VVTActivityDB:
|
|
act = VVTActivityDB()
|
|
act.id = uuid.uuid4()
|
|
act.vvt_id = kwargs.get("vvt_id", "VVT-001")
|
|
act.name = kwargs.get("name", "Test")
|
|
act.description = kwargs.get("description", None)
|
|
act.purposes = kwargs.get("purposes", [])
|
|
act.legal_bases = kwargs.get("legal_bases", [])
|
|
act.data_subject_categories = kwargs.get("data_subject_categories", [])
|
|
act.personal_data_categories = kwargs.get("personal_data_categories", [])
|
|
act.recipient_categories = kwargs.get("recipient_categories", [])
|
|
act.third_country_transfers = kwargs.get("third_country_transfers", [])
|
|
act.retention_period = kwargs.get("retention_period", {})
|
|
act.tom_description = kwargs.get("tom_description", None)
|
|
act.business_function = kwargs.get("business_function", None)
|
|
act.systems = kwargs.get("systems", [])
|
|
act.deployment_model = kwargs.get("deployment_model", None)
|
|
act.data_sources = kwargs.get("data_sources", [])
|
|
act.data_flows = kwargs.get("data_flows", [])
|
|
act.protection_level = kwargs.get("protection_level", "MEDIUM")
|
|
act.dpia_required = kwargs.get("dpia_required", False)
|
|
act.structured_toms = kwargs.get("structured_toms", {})
|
|
act.status = kwargs.get("status", "DRAFT")
|
|
act.responsible = kwargs.get("responsible", None)
|
|
act.owner = kwargs.get("owner", None)
|
|
act.last_reviewed_at = kwargs.get("last_reviewed_at", None)
|
|
act.next_review_at = kwargs.get("next_review_at", None)
|
|
act.created_by = kwargs.get("created_by", None)
|
|
act.dsfa_id = kwargs.get("dsfa_id", None)
|
|
act.created_at = datetime.utcnow()
|
|
act.updated_at = None
|
|
return act
|
|
|
|
def test_basic_conversion(self):
|
|
act = self._make_activity(vvt_id="VVT-001", name="Kundendaten")
|
|
response = _activity_to_response(act)
|
|
assert response.vvt_id == "VVT-001"
|
|
assert response.name == "Kundendaten"
|
|
assert response.status == "DRAFT"
|
|
assert response.protection_level == "MEDIUM"
|
|
|
|
def test_null_lists_become_empty(self):
|
|
act = self._make_activity()
|
|
act.purposes = None
|
|
act.legal_bases = None
|
|
response = _activity_to_response(act)
|
|
assert response.purposes == []
|
|
assert response.legal_bases == []
|
|
|
|
def test_null_dicts_become_empty(self):
|
|
act = self._make_activity()
|
|
act.retention_period = None
|
|
act.structured_toms = None
|
|
response = _activity_to_response(act)
|
|
assert response.retention_period == {}
|
|
assert response.structured_toms == {}
|
|
|
|
|
|
class TestLogAudit:
|
|
def test_creates_audit_entry(self):
|
|
mock_db = MagicMock()
|
|
act_id = uuid.uuid4()
|
|
_log_audit(
|
|
db=mock_db,
|
|
action="CREATE",
|
|
entity_type="activity",
|
|
entity_id=act_id,
|
|
changed_by="test_user",
|
|
new_values={"name": "Test"},
|
|
)
|
|
mock_db.add.assert_called_once()
|
|
added = mock_db.add.call_args[0][0]
|
|
assert added.action == "CREATE"
|
|
assert added.entity_type == "activity"
|
|
assert added.entity_id == act_id
|
|
|
|
def test_defaults_changed_by(self):
|
|
mock_db = MagicMock()
|
|
_log_audit(mock_db, "DELETE", "activity")
|
|
added = mock_db.add.call_args[0][0]
|
|
assert added.changed_by == "system"
|
|
|
|
|
|
# =============================================================================
|
|
# Consolidation Tests (Go → Python feature parity)
|
|
# =============================================================================
|
|
|
|
class TestVVTConsolidationSchemas:
|
|
"""Tests for new fields ported from Go: review dates, created_by, dsfa_id."""
|
|
|
|
def test_activity_create_with_review_dates(self):
|
|
now = datetime.now(timezone.utc)
|
|
future = now + timedelta(days=365)
|
|
req = VVTActivityCreate(
|
|
vvt_id="VVT-REV-001",
|
|
name="Review-Test",
|
|
last_reviewed_at=now,
|
|
next_review_at=future,
|
|
)
|
|
assert req.last_reviewed_at == now
|
|
assert req.next_review_at == future
|
|
|
|
def test_activity_create_sets_created_by(self):
|
|
req = VVTActivityCreate(
|
|
vvt_id="VVT-CB-001",
|
|
name="Created-By Test",
|
|
created_by="admin@example.com",
|
|
)
|
|
assert req.created_by == "admin@example.com"
|
|
|
|
def test_activity_create_created_by_defaults_none(self):
|
|
req = VVTActivityCreate(vvt_id="VVT-CB-002", name="Default Test")
|
|
assert req.created_by is None
|
|
|
|
def test_activity_create_with_dsfa_id(self):
|
|
dsfa_uuid = str(uuid.uuid4())
|
|
req = VVTActivityCreate(
|
|
vvt_id="VVT-DSFA-001",
|
|
name="DSFA-Link Test",
|
|
dsfa_id=dsfa_uuid,
|
|
)
|
|
assert req.dsfa_id == dsfa_uuid
|
|
|
|
def test_activity_update_review_dates(self):
|
|
now = datetime.now(timezone.utc)
|
|
req = VVTActivityUpdate(
|
|
last_reviewed_at=now,
|
|
next_review_at=now + timedelta(days=180),
|
|
)
|
|
data = req.model_dump(exclude_none=True)
|
|
assert "last_reviewed_at" in data
|
|
assert "next_review_at" in data
|
|
|
|
def test_activity_update_dsfa_id(self):
|
|
dsfa_uuid = str(uuid.uuid4())
|
|
req = VVTActivityUpdate(dsfa_id=dsfa_uuid)
|
|
data = req.model_dump(exclude_none=True)
|
|
assert data["dsfa_id"] == dsfa_uuid
|
|
|
|
|
|
class TestVVTConsolidationResponse:
|
|
"""Tests for new fields in response mapping."""
|
|
|
|
def _make_activity(self, **kwargs) -> VVTActivityDB:
|
|
act = VVTActivityDB()
|
|
act.id = uuid.uuid4()
|
|
act.vvt_id = kwargs.get("vvt_id", "VVT-001")
|
|
act.name = kwargs.get("name", "Test")
|
|
act.description = None
|
|
act.purposes = []
|
|
act.legal_bases = []
|
|
act.data_subject_categories = []
|
|
act.personal_data_categories = []
|
|
act.recipient_categories = []
|
|
act.third_country_transfers = []
|
|
act.retention_period = {}
|
|
act.tom_description = None
|
|
act.business_function = None
|
|
act.systems = []
|
|
act.deployment_model = None
|
|
act.data_sources = []
|
|
act.data_flows = []
|
|
act.protection_level = "MEDIUM"
|
|
act.dpia_required = False
|
|
act.structured_toms = {}
|
|
act.status = "DRAFT"
|
|
act.responsible = None
|
|
act.owner = None
|
|
act.last_reviewed_at = kwargs.get("last_reviewed_at", None)
|
|
act.next_review_at = kwargs.get("next_review_at", None)
|
|
act.created_by = kwargs.get("created_by", None)
|
|
act.dsfa_id = kwargs.get("dsfa_id", None)
|
|
act.created_at = datetime.utcnow()
|
|
act.updated_at = None
|
|
return act
|
|
|
|
def test_response_includes_review_dates(self):
|
|
now = datetime.now(timezone.utc)
|
|
future = now + timedelta(days=365)
|
|
act = self._make_activity(last_reviewed_at=now, next_review_at=future)
|
|
resp = _activity_to_response(act)
|
|
assert resp.last_reviewed_at == now
|
|
assert resp.next_review_at == future
|
|
|
|
def test_response_includes_created_by(self):
|
|
act = self._make_activity(created_by="admin@example.com")
|
|
resp = _activity_to_response(act)
|
|
assert resp.created_by == "admin@example.com"
|
|
|
|
def test_response_includes_dsfa_id(self):
|
|
dsfa_uuid = uuid.uuid4()
|
|
act = self._make_activity(dsfa_id=dsfa_uuid)
|
|
resp = _activity_to_response(act)
|
|
assert resp.dsfa_id == str(dsfa_uuid)
|
|
|
|
def test_response_null_new_fields(self):
|
|
act = self._make_activity()
|
|
resp = _activity_to_response(act)
|
|
assert resp.last_reviewed_at is None
|
|
assert resp.next_review_at is None
|
|
assert resp.created_by is None
|
|
assert resp.dsfa_id is None
|
|
|
|
|
|
class TestVVTCsvExport:
|
|
"""Tests for CSV export functionality."""
|
|
|
|
def _collect_csv_body(self, response) -> str:
|
|
"""Extract text from StreamingResponse (async generator)."""
|
|
import asyncio
|
|
async def _read():
|
|
chunks = []
|
|
async for chunk in response.body_iterator:
|
|
chunks.append(chunk)
|
|
return ''.join(chunks)
|
|
return asyncio.get_event_loop().run_until_complete(_read())
|
|
|
|
def test_export_csv_format(self):
|
|
from compliance.api.vvt_routes import _export_csv
|
|
act = VVTActivityDB()
|
|
act.id = uuid.uuid4()
|
|
act.vvt_id = "VVT-CSV-001"
|
|
act.name = "CSV Test"
|
|
act.purposes = ["Zweck A", "Zweck B"]
|
|
act.legal_bases = ["Art. 6 Abs. 1b"]
|
|
act.personal_data_categories = ["Email"]
|
|
act.data_subject_categories = ["Kunden"]
|
|
act.recipient_categories = ["IT-Dienstleister"]
|
|
act.third_country_transfers = ["USA"]
|
|
act.retention_period = {"duration": "3 Jahre"}
|
|
act.status = "APPROVED"
|
|
act.responsible = "DSB"
|
|
act.created_by = "admin"
|
|
act.created_at = datetime(2026, 1, 15, 10, 30)
|
|
act.updated_at = None
|
|
|
|
response = _export_csv([act])
|
|
text = self._collect_csv_body(response)
|
|
assert 'VVT-CSV-001' in text
|
|
assert 'CSV Test' in text
|
|
assert 'APPROVED' in text
|
|
|
|
def test_export_csv_semicolon_separator(self):
|
|
from compliance.api.vvt_routes import _export_csv
|
|
act = VVTActivityDB()
|
|
act.id = uuid.uuid4()
|
|
act.vvt_id = "VVT-SEP-001"
|
|
act.name = "Separator Test"
|
|
act.purposes = []
|
|
act.legal_bases = []
|
|
act.personal_data_categories = []
|
|
act.data_subject_categories = []
|
|
act.recipient_categories = []
|
|
act.third_country_transfers = []
|
|
act.retention_period = {}
|
|
act.status = "DRAFT"
|
|
act.responsible = ""
|
|
act.created_by = "system"
|
|
act.created_at = datetime(2026, 3, 1, 12, 0)
|
|
act.updated_at = None
|
|
|
|
response = _export_csv([act])
|
|
text = self._collect_csv_body(response)
|
|
lines = text.strip().split('\n')
|
|
header = lines[0]
|
|
assert ';' in header
|
|
assert 'ID;VVT-ID;Name' in header.replace('\ufeff', '')
|
|
|
|
def test_export_csv_empty_list(self):
|
|
from compliance.api.vvt_routes import _export_csv
|
|
response = _export_csv([])
|
|
text = self._collect_csv_body(response)
|
|
lines = text.strip().split('\n')
|
|
assert len(lines) == 1
|