feat(vvt): Go-Features nach Python portieren (Source of Truth)
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>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, timedelta, timezone
|
||||
import uuid
|
||||
|
||||
from compliance.api.schemas import (
|
||||
@@ -104,10 +104,24 @@ class TestVVTStatsResponse:
|
||||
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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -168,6 +182,10 @@ class TestActivityToResponse:
|
||||
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
|
||||
@@ -220,3 +238,196 @@ class TestLogAudit:
|
||||
_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
|
||||
|
||||
Reference in New Issue
Block a user