"""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, tenant_id="9282a473-5c95-4b3a-bf78-0ecc0ec71d3e", 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, tenant_id="9282a473-5c95-4b3a-bf78-0ecc0ec71d3e", action="DELETE", entity_type="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 # ============================================================================= # API Endpoint Tests (TestClient + mock DB) # ============================================================================= from fastapi.testclient import TestClient from fastapi import FastAPI from compliance.api.vvt_routes import router _app = FastAPI() _app.include_router(router) _client = TestClient(_app) DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" def _make_db_activity(**kwargs): """Create a mock VVTActivityDB object for query results.""" act = VVTActivityDB() act.id = kwargs.get("id", uuid.uuid4()) act.tenant_id = kwargs.get("tenant_id", DEFAULT_TENANT) act.vvt_id = kwargs.get("vvt_id", "VVT-001") act.name = kwargs.get("name", "Test Verarbeitung") act.description = kwargs.get("description", None) act.purposes = kwargs.get("purposes", ["Vertragserfuellung"]) act.legal_bases = kwargs.get("legal_bases", ["Art. 6 Abs. 1b"]) act.data_subject_categories = kwargs.get("data_subject_categories", ["Kunden"]) act.personal_data_categories = kwargs.get("personal_data_categories", ["Email"]) act.recipient_categories = kwargs.get("recipient_categories", []) act.third_country_transfers = kwargs.get("third_country_transfers", []) act.retention_period = kwargs.get("retention_period", {"duration": "3 Jahre"}) act.tom_description = kwargs.get("tom_description", None) act.business_function = kwargs.get("business_function", "IT") 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", "system") act.dsfa_id = kwargs.get("dsfa_id", None) act.created_at = kwargs.get("created_at", datetime(2026, 1, 15, 10, 0)) act.updated_at = kwargs.get("updated_at", None) return act def _make_db_org(**kwargs): """Create a mock VVTOrganizationDB object.""" org = VVTOrganizationDB() org.id = kwargs.get("id", uuid.uuid4()) org.tenant_id = kwargs.get("tenant_id", DEFAULT_TENANT) org.organization_name = kwargs.get("organization_name", "BreakPilot GmbH") org.industry = kwargs.get("industry", "IT") org.locations = kwargs.get("locations", ["Berlin"]) org.employee_count = kwargs.get("employee_count", 50) org.dpo_name = kwargs.get("dpo_name", "Max DSB") org.dpo_contact = kwargs.get("dpo_contact", "dsb@example.com") org.vvt_version = kwargs.get("vvt_version", "1.0") org.last_review_date = kwargs.get("last_review_date", None) org.next_review_date = kwargs.get("next_review_date", None) org.review_interval = kwargs.get("review_interval", "annual") org.created_at = kwargs.get("created_at", datetime(2026, 1, 1)) org.updated_at = kwargs.get("updated_at", None) return org def _make_audit_entry(**kwargs): """Create a mock VVTAuditLogDB object.""" entry = VVTAuditLogDB() entry.id = kwargs.get("id", uuid.uuid4()) entry.tenant_id = kwargs.get("tenant_id", DEFAULT_TENANT) entry.action = kwargs.get("action", "CREATE") entry.entity_type = kwargs.get("entity_type", "activity") entry.entity_id = kwargs.get("entity_id", uuid.uuid4()) entry.changed_by = kwargs.get("changed_by", "system") entry.old_values = kwargs.get("old_values", None) entry.new_values = kwargs.get("new_values", {"name": "Test"}) entry.created_at = kwargs.get("created_at", datetime(2026, 1, 15, 10, 0)) return entry @pytest.fixture def mock_db(): from classroom_engine.database import get_db from compliance.api.tenant_utils import get_tenant_id db = MagicMock() _app.dependency_overrides[get_db] = lambda: db _app.dependency_overrides[get_tenant_id] = lambda: DEFAULT_TENANT yield db _app.dependency_overrides.clear() class TestExportEndpoint: """Tests for GET /vvt/export (JSON and CSV).""" def test_export_json_with_activities(self, mock_db): act = _make_db_activity(vvt_id="VVT-EXP-001", name="Export Test") org = _make_db_org() # mock chained query for org mock_db.query.return_value.filter.return_value.order_by.return_value.first.return_value = org # mock chained query for activities mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [act] resp = _client.get("/vvt/export?format=json") assert resp.status_code == 200 data = resp.json() assert "exported_at" in data assert "organization" in data assert data["organization"]["name"] == "BreakPilot GmbH" assert len(data["activities"]) == 1 assert data["activities"][0]["vvt_id"] == "VVT-EXP-001" def test_export_json_empty_dataset(self, mock_db): mock_db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [] resp = _client.get("/vvt/export?format=json") assert resp.status_code == 200 data = resp.json() assert data["organization"] is None assert data["activities"] == [] def test_export_csv_returns_streaming_response(self, mock_db): act = _make_db_activity(vvt_id="VVT-CSV-E01", name="CSV Endpoint Test") mock_db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [act] resp = _client.get("/vvt/export?format=csv") assert resp.status_code == 200 assert "text/csv" in resp.headers.get("content-type", "") assert "attachment" in resp.headers.get("content-disposition", "") body = resp.text assert "VVT-CSV-E01" in body assert "CSV Endpoint Test" in body def test_export_invalid_format_rejected(self, mock_db): resp = _client.get("/vvt/export?format=xml") assert resp.status_code == 422 # validation error class TestStatsEndpoint: """Tests for GET /vvt/stats.""" def test_stats_empty_tenant(self, mock_db): mock_db.query.return_value.filter.return_value.all.return_value = [] resp = _client.get("/vvt/stats") assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert data["by_status"] == {} assert data["dpia_required_count"] == 0 assert data["overdue_review_count"] == 0 def test_stats_with_activities(self, mock_db): past = datetime(2025, 1, 1, tzinfo=timezone.utc) acts = [ _make_db_activity(status="DRAFT", business_function="HR", dpia_required=True, next_review_at=past), _make_db_activity(status="APPROVED", business_function="IT", dpia_required=False), _make_db_activity(status="DRAFT", business_function="HR", dpia_required=False, third_country_transfers=["USA"]), ] mock_db.query.return_value.filter.return_value.all.return_value = acts resp = _client.get("/vvt/stats") assert resp.status_code == 200 data = resp.json() assert data["total"] == 3 assert data["by_status"]["DRAFT"] == 2 assert data["by_status"]["APPROVED"] == 1 assert data["by_business_function"]["HR"] == 2 assert data["by_business_function"]["IT"] == 1 assert data["dpia_required_count"] == 1 assert data["third_country_count"] == 1 assert data["draft_count"] == 2 assert data["approved_count"] == 1 assert data["overdue_review_count"] == 1 class TestAuditLogEndpoint: """Tests for GET /vvt/audit-log.""" def test_audit_log_returns_entries(self, mock_db): entry = _make_audit_entry(action="CREATE", entity_type="activity") mock_db.query.return_value.filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [entry] resp = _client.get("/vvt/audit-log") assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["action"] == "CREATE" assert data[0]["entity_type"] == "activity" def test_audit_log_empty(self, mock_db): mock_db.query.return_value.filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [] resp = _client.get("/vvt/audit-log") assert resp.status_code == 200 assert resp.json() == [] def test_audit_log_pagination_params(self, mock_db): mock_db.query.return_value.filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [] resp = _client.get("/vvt/audit-log?limit=10&offset=20") assert resp.status_code == 200 class TestVersioningEndpoints: """Tests for GET /vvt/activities/{id}/versions and /versions/{v}.""" @patch("compliance.api.versioning_utils.list_versions") def test_list_versions_returns_list(self, mock_list_versions, mock_db): act_id = str(uuid.uuid4()) mock_list_versions.return_value = [ {"id": str(uuid.uuid4()), "version_number": 2, "status": "draft", "change_summary": "Updated name", "changed_sections": [], "created_by": "admin", "approved_by": None, "approved_at": None, "created_at": "2026-01-15T10:00:00"}, {"id": str(uuid.uuid4()), "version_number": 1, "status": "draft", "change_summary": "Initial", "changed_sections": [], "created_by": "system", "approved_by": None, "approved_at": None, "created_at": "2026-01-14T09:00:00"}, ] resp = _client.get(f"/vvt/activities/{act_id}/versions") assert resp.status_code == 200 data = resp.json() assert len(data) == 2 assert data[0]["version_number"] == 2 assert data[1]["version_number"] == 1 @patch("compliance.api.versioning_utils.list_versions") def test_list_versions_empty(self, mock_list_versions, mock_db): act_id = str(uuid.uuid4()) mock_list_versions.return_value = [] resp = _client.get(f"/vvt/activities/{act_id}/versions") assert resp.status_code == 200 assert resp.json() == [] @patch("compliance.api.versioning_utils.get_version") def test_get_specific_version(self, mock_get_version, mock_db): act_id = str(uuid.uuid4()) mock_get_version.return_value = { "id": str(uuid.uuid4()), "version_number": 1, "status": "approved", "snapshot": {"name": "Test", "status": "APPROVED"}, "change_summary": "Initial version", "changed_sections": ["name", "status"], "created_by": "admin", "approved_by": "dpo", "approved_at": "2026-01-16T12:00:00", "created_at": "2026-01-15T10:00:00", } resp = _client.get(f"/vvt/activities/{act_id}/versions/1") assert resp.status_code == 200 data = resp.json() assert data["version_number"] == 1 assert data["snapshot"]["name"] == "Test" assert data["approved_by"] == "dpo" @patch("compliance.api.versioning_utils.get_version") def test_get_version_not_found(self, mock_get_version, mock_db): act_id = str(uuid.uuid4()) mock_get_version.return_value = None resp = _client.get(f"/vvt/activities/{act_id}/versions/999") assert resp.status_code == 404 assert "not found" in resp.json()["detail"].lower() class TestExportCsvEdgeCases: """Additional edge cases for CSV export helper.""" def _collect_csv_body(self, response) -> str: 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_with_third_country_transfers(self): from compliance.api.vvt_routes import _export_csv act = _make_db_activity( third_country_transfers=["USA", "China"], vvt_id="VVT-TC-001", name="Third Country Test", ) response = _export_csv([act]) text = self._collect_csv_body(response) assert "Ja" in text # third_country_transfers truthy -> "Ja" def test_export_csv_no_third_country_transfers(self): from compliance.api.vvt_routes import _export_csv act = _make_db_activity( third_country_transfers=[], vvt_id="VVT-NTC-001", name="No Third Country", ) response = _export_csv([act]) text = self._collect_csv_body(response) assert "Nein" in text # empty list -> "Nein" def test_export_csv_multiple_activities(self): from compliance.api.vvt_routes import _export_csv acts = [ _make_db_activity(vvt_id="VVT-M-001", name="First"), _make_db_activity(vvt_id="VVT-M-002", name="Second"), _make_db_activity(vvt_id="VVT-M-003", name="Third"), ] response = _export_csv(acts) text = self._collect_csv_body(response) lines = text.strip().split('\n') # 1 header + 3 data rows assert len(lines) == 4 assert "VVT-M-001" in lines[1] assert "VVT-M-002" in lines[2] assert "VVT-M-003" in lines[3] def test_export_csv_content_disposition_filename(self): from compliance.api.vvt_routes import _export_csv response = _export_csv([]) assert "vvt_export_" in response.headers.get("content-disposition", "") assert ".csv" in response.headers.get("content-disposition", "")