"""Tests for VVT Library + Template routes (vvt_library_routes.py, vvt_library_models.py).""" import pytest import uuid from unittest.mock import MagicMock, patch from datetime import datetime from compliance.db.vvt_library_models import ( VVTLibDataSubjectDB, VVTLibDataCategoryDB, VVTLibRecipientDB, VVTLibLegalBasisDB, VVTLibRetentionRuleDB, VVTLibTransferMechanismDB, VVTLibPurposeDB, VVTLibTomDB, VVTProcessTemplateDB, ) from compliance.db.vvt_models import VVTActivityDB from compliance.api.vvt_library_routes import ( _row_to_dict, _resolve_ids, _template_to_dict, ) # ============================================================================= # Model Tests # ============================================================================= class TestLibraryModels: def test_data_subject_model(self): ds = VVTLibDataSubjectDB( id="EMPLOYEES", label_de="Beschaeftigte", description_de="Aktuelle Mitarbeiter", art9_relevant=False, typical_for=["hr"], sort_order=1, ) assert ds.id == "EMPLOYEES" assert ds.label_de == "Beschaeftigte" assert ds.art9_relevant is False def test_data_category_hierarchical(self): parent = VVTLibDataCategoryDB( id="IDENTIFICATION", label_de="Identifikationsdaten", is_art9=False, is_art10=False, risk_weight=2, ) child = VVTLibDataCategoryDB( id="NAME", parent_id="IDENTIFICATION", label_de="Name", is_art9=False, is_art10=False, risk_weight=1, ) assert parent.parent_id is None assert child.parent_id == "IDENTIFICATION" def test_data_category_art9(self): cat = VVTLibDataCategoryDB( id="HEALTH_DATA", parent_id="ART9_SPECIAL", label_de="Gesundheitsdaten", is_art9=True, risk_weight=5, ) assert cat.is_art9 is True assert cat.risk_weight == 5 def test_recipient_model(self): rec = VVTLibRecipientDB( id="PROCESSOR_PAYROLL", type="PROCESSOR", label_de="Lohnabrechnungsdienstleister", is_third_country=False, country="DE", ) assert rec.type == "PROCESSOR" assert rec.is_third_country is False def test_legal_basis_model(self): lb = VVTLibLegalBasisDB( id="ART6_1A", article="Art. 6 Abs. 1 lit. a", type="CONSENT", label_de="Einwilligung", is_art9=False, ) assert lb.type == "CONSENT" assert lb.article == "Art. 6 Abs. 1 lit. a" def test_retention_rule_model(self): rr = VVTLibRetentionRuleDB( id="HGB_257_10Y", label_de="10 Jahre (HGB)", duration=10, duration_unit="YEARS", start_event="Ende des Kalenderjahres", ) assert rr.duration == 10 assert rr.duration_unit == "YEARS" def test_transfer_mechanism_model(self): tm = VVTLibTransferMechanismDB( id="SCC_PROCESSOR", label_de="Standardvertragsklauseln", article="Art. 46", requires_tia=True, ) assert tm.requires_tia is True def test_purpose_model(self): p = VVTLibPurposeDB( id="PAYROLL", label_de="Gehaltsabrechnung", typical_legal_basis="BDSG_26", typical_for=["hr"], ) assert p.typical_legal_basis == "BDSG_26" def test_tom_model(self): t = VVTLibTomDB( id="AC_RBAC", category="accessControl", label_de="Rollenbasierte Zugriffskontrolle", art32_reference="Art. 32 Abs. 1 lit. b", ) assert t.category == "accessControl" def test_process_template_model(self): tpl = VVTProcessTemplateDB( id="hr-mitarbeiterverwaltung", name="Mitarbeiterverwaltung", business_function="hr", purpose_refs=["EMPLOYMENT_ADMIN", "PAYROLL"], data_subject_refs=["EMPLOYEES"], retention_rule_ref="HGB_257_10Y", is_system=True, ) assert tpl.is_system is True assert len(tpl.purpose_refs) == 2 # ============================================================================= # Helper Function Tests # ============================================================================= class TestRowToDict: def test_basic_fields(self): row = MagicMock() row.id = "TEST" row.label_de = "Test Label" row.description_de = "Test Description" row.sort_order = 1 result = _row_to_dict(row) assert result["id"] == "TEST" assert result["label_de"] == "Test Label" assert result["description_de"] == "Test Description" def test_extra_fields(self): row = MagicMock() row.id = "ART6_1A" row.label_de = "Einwilligung" row.description_de = None row.sort_order = 1 row.article = "Art. 6" row.type = "CONSENT" result = _row_to_dict(row, ["article", "type"]) assert result["article"] == "Art. 6" assert result["type"] == "CONSENT" assert "description_de" not in result # None is excluded def test_none_extra_fields_excluded(self): row = MagicMock() row.id = "X" row.label_de = "X" row.description_de = None row.sort_order = 0 row.country = None result = _row_to_dict(row, ["country"]) assert "country" not in result class TestTemplateToDict: def test_template_to_dict(self): tpl = MagicMock() tpl.id = "hr-test" tpl.name = "HR Test" tpl.description = "Test desc" tpl.business_function = "hr" tpl.purpose_refs = ["PAYROLL"] tpl.legal_basis_refs = ["BDSG_26"] tpl.data_subject_refs = ["EMPLOYEES"] tpl.data_category_refs = ["NAME"] tpl.recipient_refs = ["INTERNAL_HR"] tpl.tom_refs = ["AC_RBAC"] tpl.transfer_mechanism_refs = [] tpl.retention_rule_ref = "HGB_257_10Y" tpl.typical_systems = ["SAP"] tpl.protection_level = "HIGH" tpl.dpia_required = False tpl.risk_score = 3 tpl.tags = ["hr"] tpl.is_system = True tpl.sort_order = 1 result = _template_to_dict(tpl) assert result["id"] == "hr-test" assert result["name"] == "HR Test" assert result["purpose_refs"] == ["PAYROLL"] assert result["retention_rule_ref"] == "HGB_257_10Y" def test_template_empty_refs(self): tpl = MagicMock() tpl.id = "empty" tpl.name = "Empty" tpl.description = None tpl.business_function = "other" tpl.purpose_refs = None tpl.legal_basis_refs = None tpl.data_subject_refs = None tpl.data_category_refs = None tpl.recipient_refs = None tpl.tom_refs = None tpl.transfer_mechanism_refs = None tpl.retention_rule_ref = None tpl.typical_systems = None tpl.protection_level = None tpl.dpia_required = None tpl.risk_score = None tpl.tags = None tpl.is_system = True tpl.sort_order = 0 result = _template_to_dict(tpl) assert result["purpose_refs"] == [] assert result["retention_rule_ref"] is None assert result["protection_level"] == "MEDIUM" class TestResolveIds: def test_resolve_ids_basic(self): mock_db = MagicMock() row1 = MagicMock() row1.id = "PAYROLL" row1.label_de = "Gehaltsabrechnung" row2 = MagicMock() row2.id = "CRM" row2.label_de = "Kundenbeziehungsmanagement" mock_db.query.return_value.filter.return_value.all.return_value = [row1, row2] result = _resolve_ids(mock_db, VVTLibPurposeDB, ["PAYROLL", "CRM"]) assert result == ["Gehaltsabrechnung", "Kundenbeziehungsmanagement"] def test_resolve_ids_empty(self): mock_db = MagicMock() result = _resolve_ids(mock_db, VVTLibPurposeDB, []) assert result == [] def test_resolve_ids_missing_fallback(self): mock_db = MagicMock() row1 = MagicMock() row1.id = "PAYROLL" row1.label_de = "Gehaltsabrechnung" mock_db.query.return_value.filter.return_value.all.return_value = [row1] result = _resolve_ids(mock_db, VVTLibPurposeDB, ["PAYROLL", "UNKNOWN"]) assert result == ["Gehaltsabrechnung", "UNKNOWN"] # ============================================================================= # FastAPI Endpoint Tests (via TestClient) # ============================================================================= class TestLibraryEndpoints: """Tests using FastAPI app.dependency_overrides for DB mocking.""" @pytest.fixture def client(self): from fastapi.testclient import TestClient from fastapi import FastAPI from classroom_engine.database import get_db from compliance.api.tenant_utils import get_tenant_id from compliance.api.vvt_library_routes import router app = FastAPI() app.include_router(router, prefix="/api/compliance") mock_db = MagicMock() app.dependency_overrides[get_db] = lambda: mock_db app.dependency_overrides[get_tenant_id] = lambda: "test-tenant" client = TestClient(app) client.mock_db = mock_db return client # Libraries overview def test_libraries_overview(self, client): client.mock_db.query.return_value.count.return_value = 5 res = client.get("/api/compliance/vvt/libraries") assert res.status_code == 200 data = res.json() assert "libraries" in data assert len(data["libraries"]) == 8 # Data Subjects def test_list_data_subjects(self, client): row = MagicMock() row.id = "EMPLOYEES" row.label_de = "Beschaeftigte" row.description_de = "Aktuelle Mitarbeiter" row.art9_relevant = False row.typical_for = ["hr"] row.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/data-subjects") assert res.status_code == 200 data = res.json() assert len(data) == 1 assert data[0]["id"] == "EMPLOYEES" def test_list_data_subjects_filter(self, client): row1 = MagicMock() row1.id = "EMPLOYEES" row1.label_de = "Beschaeftigte" row1.description_de = None row1.art9_relevant = False row1.typical_for = ["hr"] row1.sort_order = 1 row2 = MagicMock() row2.id = "CUSTOMERS" row2.label_de = "Kunden" row2.description_de = None row2.art9_relevant = False row2.typical_for = ["sales_crm"] row2.sort_order = 3 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row1, row2] res = client.get("/api/compliance/vvt/libraries/data-subjects?typical_for=hr") assert res.status_code == 200 data = res.json() assert len(data) == 1 assert data[0]["id"] == "EMPLOYEES" # Data Categories def test_list_data_categories_flat(self, client): row = MagicMock() row.id = "NAME" row.label_de = "Name" row.description_de = None row.parent_id = "IDENTIFICATION" row.is_art9 = False row.is_art10 = False row.risk_weight = 1 row.default_retention_rule = None row.default_legal_basis = None row.sort_order = 10 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/data-categories?flat=true") assert res.status_code == 200 data = res.json() assert len(data) == 1 assert data[0]["parent_id"] == "IDENTIFICATION" def test_list_data_categories_tree(self, client): parent = MagicMock() parent.id = "IDENTIFICATION" parent.label_de = "Identifikationsdaten" parent.description_de = None parent.parent_id = None parent.is_art9 = False parent.is_art10 = False parent.risk_weight = 2 parent.default_retention_rule = None parent.default_legal_basis = None parent.sort_order = 1 child = MagicMock() child.id = "NAME" child.label_de = "Name" child.description_de = None child.parent_id = "IDENTIFICATION" child.is_art9 = False child.is_art10 = False child.risk_weight = 1 child.default_retention_rule = None child.default_legal_basis = None child.sort_order = 10 client.mock_db.query.return_value.order_by.return_value.all.return_value = [parent, child] res = client.get("/api/compliance/vvt/libraries/data-categories") assert res.status_code == 200 data = res.json() assert len(data) == 1 # only parent at top level assert data[0]["id"] == "IDENTIFICATION" assert len(data[0]["children"]) == 1 assert data[0]["children"][0]["id"] == "NAME" def test_list_data_categories_filter_art9(self, client): row = MagicMock() row.id = "HEALTH_DATA" row.label_de = "Gesundheitsdaten" row.description_de = None row.parent_id = "ART9_SPECIAL" row.is_art9 = True row.is_art10 = False row.risk_weight = 5 row.default_retention_rule = None row.default_legal_basis = None row.sort_order = 80 client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row] # Adjusting for chained filter calls client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/data-categories?is_art9=true&flat=true") assert res.status_code == 200 # Recipients def test_list_recipients(self, client): row = MagicMock() row.id = "INTERNAL_HR" row.label_de = "Personalabteilung" row.description_de = None row.type = "INTERNAL" row.is_third_country = False row.country = "DE" row.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/recipients") assert res.status_code == 200 data = res.json() assert data[0]["type"] == "INTERNAL" def test_list_recipients_filter_type(self, client): row = MagicMock() row.id = "PROCESSOR_PAYROLL" row.label_de = "Lohnabrechnung" row.description_de = None row.type = "PROCESSOR" row.is_third_country = False row.country = "DE" row.sort_order = 7 client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/recipients?type=PROCESSOR") assert res.status_code == 200 # Legal Bases def test_list_legal_bases(self, client): row = MagicMock() row.id = "ART6_1A" row.label_de = "Einwilligung" row.description_de = None row.article = "Art. 6" row.type = "CONSENT" row.is_art9 = False row.typical_national_law = None row.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/legal-bases") assert res.status_code == 200 data = res.json() assert data[0]["article"] == "Art. 6" # Retention Rules def test_list_retention_rules(self, client): row = MagicMock() row.id = "HGB_257_10Y" row.label_de = "10 Jahre (HGB)" row.description_de = None row.legal_basis = "HGB § 257" row.duration = 10 row.duration_unit = "YEARS" row.start_event = "Ende des Kalenderjahres" row.deletion_procedure = "Vernichtung" row.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/retention-rules") assert res.status_code == 200 data = res.json() assert data[0]["duration"] == 10 assert data[0]["duration_unit"] == "YEARS" # Transfer Mechanisms def test_list_transfer_mechanisms(self, client): row = MagicMock() row.id = "SCC_PROCESSOR" row.label_de = "SCC" row.description_de = None row.article = "Art. 46" row.requires_tia = True row.sort_order = 3 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/transfer-mechanisms") assert res.status_code == 200 data = res.json() assert data[0]["requires_tia"] is True # Purposes def test_list_purposes(self, client): row = MagicMock() row.id = "PAYROLL" row.label_de = "Gehaltsabrechnung" row.description_de = None row.typical_legal_basis = "BDSG_26" row.typical_for = ["hr"] row.sort_order = 2 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/purposes") assert res.status_code == 200 data = res.json() assert data[0]["typical_legal_basis"] == "BDSG_26" # TOMs def test_list_toms(self, client): row = MagicMock() row.id = "AC_RBAC" row.label_de = "RBAC" row.description_de = None row.category = "accessControl" row.art32_reference = "Art. 32" row.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/toms") assert res.status_code == 200 data = res.json() assert data[0]["category"] == "accessControl" def test_list_toms_filter(self, client): row = MagicMock() row.id = "CONF_ENCRYPTION_REST" row.label_de = "Verschluesselung" row.description_de = None row.category = "confidentiality" row.art32_reference = "Art. 32" row.sort_order = 5 client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row] res = client.get("/api/compliance/vvt/libraries/toms?category=confidentiality") assert res.status_code == 200 # Templates def test_list_templates(self, client): tpl = MagicMock() tpl.id = "hr-test" tpl.name = "HR Test" tpl.description = "Test" tpl.business_function = "hr" tpl.purpose_refs = ["PAYROLL"] tpl.legal_basis_refs = ["BDSG_26"] tpl.data_subject_refs = ["EMPLOYEES"] tpl.data_category_refs = ["NAME"] tpl.recipient_refs = [] tpl.tom_refs = ["AC_RBAC"] tpl.transfer_mechanism_refs = [] tpl.retention_rule_ref = "HGB_257_10Y" tpl.typical_systems = ["SAP"] tpl.protection_level = "HIGH" tpl.dpia_required = False tpl.risk_score = 3 tpl.tags = ["hr"] tpl.is_system = True tpl.sort_order = 1 client.mock_db.query.return_value.order_by.return_value.all.return_value = [tpl] res = client.get("/api/compliance/vvt/templates") assert res.status_code == 200 data = res.json() assert len(data) == 1 assert data[0]["id"] == "hr-test" def test_list_templates_filter_bf(self, client): client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [] res = client.get("/api/compliance/vvt/templates?business_function=finance") assert res.status_code == 200 def test_list_templates_search(self, client): client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [] res = client.get("/api/compliance/vvt/templates?search=gehalts") assert res.status_code == 200 def test_get_template(self, client): tpl = MagicMock() tpl.id = "hr-test" tpl.name = "HR Test" tpl.description = None tpl.business_function = "hr" tpl.purpose_refs = ["PAYROLL"] tpl.legal_basis_refs = [] tpl.data_subject_refs = [] tpl.data_category_refs = [] tpl.recipient_refs = [] tpl.tom_refs = [] tpl.transfer_mechanism_refs = [] tpl.retention_rule_ref = None tpl.typical_systems = [] tpl.protection_level = "MEDIUM" tpl.dpia_required = False tpl.risk_score = None tpl.tags = [] tpl.is_system = True tpl.sort_order = 0 client.mock_db.query.return_value.filter.return_value.first.return_value = tpl # For label resolution queries client.mock_db.query.return_value.filter.return_value.all.return_value = [] res = client.get("/api/compliance/vvt/templates/hr-test") assert res.status_code == 200 data = res.json() assert data["id"] == "hr-test" def test_get_template_not_found(self, client): client.mock_db.query.return_value.filter.return_value.first.return_value = None res = client.get("/api/compliance/vvt/templates/nonexistent") assert res.status_code == 404 # Template instantiation def test_instantiate_template(self, client): tpl = MagicMock() tpl.id = "hr-test" tpl.name = "HR Test" tpl.description = "Test" tpl.business_function = "hr" tpl.purpose_refs = ["PAYROLL"] tpl.legal_basis_refs = ["BDSG_26"] tpl.data_subject_refs = ["EMPLOYEES"] tpl.data_category_refs = ["NAME"] tpl.recipient_refs = ["INTERNAL_HR"] tpl.tom_refs = ["AC_RBAC"] tpl.transfer_mechanism_refs = [] tpl.retention_rule_ref = "HGB_257_10Y" tpl.typical_systems = ["SAP"] tpl.protection_level = "HIGH" tpl.dpia_required = False tpl.risk_score = 3 purpose_row = MagicMock() purpose_row.id = "PAYROLL" purpose_row.label_de = "Gehaltsabrechnung" legal_row = MagicMock() legal_row.id = "BDSG_26" legal_row.label_de = "Beschaeftigtenverhaeltnis" subject_row = MagicMock() subject_row.id = "EMPLOYEES" subject_row.label_de = "Beschaeftigte" cat_row = MagicMock() cat_row.id = "NAME" cat_row.label_de = "Name" rec_row = MagicMock() rec_row.id = "INTERNAL_HR" rec_row.label_de = "Personalabteilung" rr_row = MagicMock() rr_row.id = "HGB_257_10Y" rr_row.label_de = "10 Jahre (HGB)" rr_row.legal_basis = "HGB § 257" rr_row.deletion_procedure = "Vernichtung" rr_row.duration = 10 rr_row.duration_unit = "YEARS" tom_row = MagicMock() tom_row.id = "AC_RBAC" tom_row.label_de = "RBAC" tom_row.category = "accessControl" def mock_query(model): mock_q = MagicMock() if model == VVTProcessTemplateDB: mock_q.filter.return_value.first.return_value = tpl elif model == VVTActivityDB: mock_q.filter.return_value.count.return_value = 5 elif model == VVTLibPurposeDB: mock_q.filter.return_value.all.return_value = [purpose_row] elif model == VVTLibLegalBasisDB: mock_q.filter.return_value.all.return_value = [legal_row] elif model == VVTLibDataSubjectDB: mock_q.filter.return_value.all.return_value = [subject_row] elif model == VVTLibDataCategoryDB: mock_q.filter.return_value.all.return_value = [cat_row] elif model == VVTLibRecipientDB: mock_q.filter.return_value.all.return_value = [rec_row] elif model == VVTLibRetentionRuleDB: mock_q.filter.return_value.first.return_value = rr_row elif model == VVTLibTomDB: mock_q.filter.return_value.all.return_value = [tom_row] return mock_q client.mock_db.query.side_effect = mock_query created_id = uuid.uuid4() created_at = datetime.utcnow() def mock_add(obj): if hasattr(obj, 'vvt_id'): obj.id = created_id obj.created_at = created_at obj.updated_at = created_at client.mock_db.add.side_effect = mock_add client.mock_db.flush.return_value = None client.mock_db.commit.return_value = None client.mock_db.refresh.return_value = None res = client.post( "/api/compliance/vvt/templates/hr-test/instantiate", headers={"X-Tenant-ID": "test-tenant", "X-User-ID": "test-user"} ) # The response contains the created activity — verify it was created assert res.status_code == 201 data = res.json() assert data["name"] == "HR Test" assert data["purpose_refs"] == ["PAYROLL"] assert data["source_template_id"] == "hr-test" def test_instantiate_template_not_found(self, client): def mock_query(model): mock_q = MagicMock() mock_q.filter.return_value.first.return_value = None return mock_q client.mock_db.query.side_effect = mock_query res = client.post( "/api/compliance/vvt/templates/nonexistent/instantiate", headers={"X-Tenant-ID": "test-tenant"} ) assert res.status_code == 404 # ============================================================================= # Completeness Calculation Tests # ============================================================================= class TestCompletenessCalculation: def test_completeness_full(self): from compliance.api.vvt_routes import _calculate_completeness act = MagicMock() act.name = "Test Activity" act.purposes = ["Zweck 1"] act.purpose_refs = None act.legal_bases = [{"type": "ART6_1B"}] act.legal_basis_refs = None act.data_subject_categories = ["Mitarbeiter"] act.data_subject_refs = None act.personal_data_categories = ["Name"] act.data_category_refs = None act.recipient_categories = [{"type": "INTERNAL", "name": "HR"}] act.recipient_refs = None act.third_country_transfers = [] act.transfer_mechanism_refs = None act.retention_period = {"description": "10 Jahre"} act.retention_rule_ref = None act.tom_description = "Verschluesselung" act.tom_refs = None act.structured_toms = {} act.responsible = "Max Mustermann" act.dpia_required = False act.dsfa_id = None result = _calculate_completeness(act) assert result["score"] == 100 assert result["missing"] == [] def test_completeness_empty(self): from compliance.api.vvt_routes import _calculate_completeness act = MagicMock() act.name = "" act.purposes = [] act.purpose_refs = None act.legal_bases = [] act.legal_basis_refs = None act.data_subject_categories = [] act.data_subject_refs = None act.personal_data_categories = [] act.data_category_refs = None act.recipient_categories = [] act.recipient_refs = None act.third_country_transfers = [] act.transfer_mechanism_refs = None act.retention_period = {} act.retention_rule_ref = None act.tom_description = "" act.tom_refs = None act.structured_toms = {} act.responsible = "" act.dpia_required = False act.dsfa_id = None result = _calculate_completeness(act) assert result["score"] < 50 assert "name" in result["missing"] assert "purposes" in result["missing"] def test_completeness_with_refs_only(self): from compliance.api.vvt_routes import _calculate_completeness act = MagicMock() act.name = "Test" act.purposes = [] act.purpose_refs = ["PAYROLL"] # refs fill the gap act.legal_bases = [] act.legal_basis_refs = ["BDSG_26"] act.data_subject_categories = [] act.data_subject_refs = ["EMPLOYEES"] act.personal_data_categories = [] act.data_category_refs = ["NAME"] act.recipient_categories = [] act.recipient_refs = ["INTERNAL_HR"] act.third_country_transfers = [] act.transfer_mechanism_refs = None act.retention_period = {} act.retention_rule_ref = "HGB_257_10Y" act.tom_description = "" act.tom_refs = ["AC_RBAC"] act.structured_toms = {} act.responsible = "Test Person" act.dpia_required = False act.dsfa_id = None result = _calculate_completeness(act) assert result["score"] == 100 assert result["missing"] == [] def test_completeness_dpia_warning(self): from compliance.api.vvt_routes import _calculate_completeness act = MagicMock() act.name = "Test" act.purposes = ["Zweck"] act.purpose_refs = None act.legal_bases = [{"type": "ART6_1B"}] act.legal_basis_refs = None act.data_subject_categories = ["Mitarbeiter"] act.data_subject_refs = None act.personal_data_categories = ["Name"] act.data_category_refs = None act.recipient_categories = [{"type": "INTERNAL", "name": "HR"}] act.recipient_refs = None act.third_country_transfers = [{"country": "US"}] act.transfer_mechanism_refs = None act.retention_period = {"description": "10 Jahre"} act.retention_rule_ref = None act.tom_description = "TOM" act.tom_refs = None act.structured_toms = {} act.responsible = "Max" act.dpia_required = True act.dsfa_id = None result = _calculate_completeness(act) assert "dpia_required_but_no_dsfa_linked" in result["warnings"] assert "third_country_transfer_without_mechanism" in result["warnings"] def test_completeness_partial(self): from compliance.api.vvt_routes import _calculate_completeness act = MagicMock() act.name = "Test" act.purposes = ["Zweck"] act.purpose_refs = None act.legal_bases = [] act.legal_basis_refs = None act.data_subject_categories = ["Mitarbeiter"] act.data_subject_refs = None act.personal_data_categories = [] act.data_category_refs = None act.recipient_categories = [] act.recipient_refs = None act.third_country_transfers = [] act.transfer_mechanism_refs = None act.retention_period = {} act.retention_rule_ref = None act.tom_description = "" act.tom_refs = None act.structured_toms = {} act.responsible = "" act.dpia_required = False act.dsfa_id = None result = _calculate_completeness(act) assert 30 <= result["score"] <= 50 assert "legal_bases" in result["missing"] assert "data_categories" in result["missing"] assert "recipients" in result["missing"] # ============================================================================= # VVT Activity Schema Tests — new ref fields # ============================================================================= class TestVVTActivityCreateWithRefs: def test_create_with_refs(self): from compliance.api.schemas import VVTActivityCreate req = VVTActivityCreate( vvt_id="VVT-010", name="Test mit Refs", purpose_refs=["PAYROLL", "CRM"], legal_basis_refs=["BDSG_26"], data_subject_refs=["EMPLOYEES"], data_category_refs=["NAME", "SALARY_DATA"], recipient_refs=["INTERNAL_HR"], retention_rule_ref="HGB_257_10Y", tom_refs=["AC_RBAC", "CONF_ENCRYPTION_REST"], source_template_id="hr-mitarbeiterverwaltung", risk_score=3, ) assert req.purpose_refs == ["PAYROLL", "CRM"] assert req.retention_rule_ref == "HGB_257_10Y" assert req.source_template_id == "hr-mitarbeiterverwaltung" assert req.risk_score == 3 def test_create_without_refs_backward_compat(self): from compliance.api.schemas import VVTActivityCreate req = VVTActivityCreate( vvt_id="VVT-011", name="Test ohne Refs", purposes=["Vertragserfuellung"], legal_bases=["Art. 6 Abs. 1b"], ) assert req.purpose_refs is None assert req.retention_rule_ref is None assert req.source_template_id is None def test_serialization_with_refs(self): from compliance.api.schemas import VVTActivityCreate req = VVTActivityCreate( vvt_id="VVT-012", name="Test", purpose_refs=["PAYROLL"], linked_loeschfristen_ids=["uuid-1"], ) data = req.model_dump() assert data["purpose_refs"] == ["PAYROLL"] assert data["linked_loeschfristen_ids"] == ["uuid-1"] class TestVVTActivityUpdateWithRefs: def test_partial_update_refs(self): from compliance.api.schemas import VVTActivityUpdate req = VVTActivityUpdate( purpose_refs=["RECRUITING"], retention_rule_ref="AGG_15_6M", ) data = req.model_dump(exclude_none=True) assert data["purpose_refs"] == ["RECRUITING"] assert data["retention_rule_ref"] == "AGG_15_6M" assert "name" not in data def test_update_without_refs_backward_compat(self): from compliance.api.schemas import VVTActivityUpdate req = VVTActivityUpdate(status="APPROVED") data = req.model_dump(exclude_none=True) assert "purpose_refs" not in data assert "status" in data class TestVVTActivityResponseWithRefs: def test_response_with_refs(self): from compliance.api.schemas import VVTActivityResponse resp = VVTActivityResponse( id="test-id", vvt_id="VVT-010", name="Test", purpose_refs=["PAYROLL"], legal_basis_refs=["BDSG_26"], source_template_id="hr-test", risk_score=3, art30_completeness={"score": 85, "missing": ["recipients"]}, created_at=datetime.utcnow(), ) assert resp.purpose_refs == ["PAYROLL"] assert resp.art30_completeness["score"] == 85 def test_response_without_refs_backward_compat(self): from compliance.api.schemas import VVTActivityResponse resp = VVTActivityResponse( id="test-id", vvt_id="VVT-011", name="Test", created_at=datetime.utcnow(), ) assert resp.purpose_refs is None assert resp.retention_rule_ref is None assert resp.art30_completeness is None # ============================================================================= # VVT Activity DB Model — new columns # ============================================================================= class TestVVTActivityDBNewColumns: def test_new_columns_exist(self): act = VVTActivityDB( tenant_id="test", vvt_id="VVT-001", name="Test", purpose_refs=["PAYROLL"], legal_basis_refs=["BDSG_26"], data_subject_refs=["EMPLOYEES"], data_category_refs=["NAME"], recipient_refs=["INTERNAL_HR"], retention_rule_ref="HGB_257_10Y", transfer_mechanism_refs=None, tom_refs=["AC_RBAC"], linked_loeschfristen_ids=["uuid-1"], linked_tom_measure_ids=None, source_template_id="hr-test", risk_score=3, art30_completeness={"score": 80, "missing": ["recipients"]}, ) assert act.purpose_refs == ["PAYROLL"] assert act.retention_rule_ref == "HGB_257_10Y" assert act.source_template_id == "hr-test" def test_new_columns_null_defaults(self): act = VVTActivityDB( tenant_id="test", vvt_id="VVT-002", name="Test ohne Refs", ) assert act.purpose_refs is None assert act.retention_rule_ref is None assert act.source_template_id is None assert act.art30_completeness is None # ============================================================================= # _activity_to_response with refs # ============================================================================= class TestActivityToResponseWithRefs: def test_includes_ref_fields(self): from compliance.api.vvt_routes import _activity_to_response act = MagicMock() act.id = uuid.uuid4() act.vvt_id = "VVT-001" act.name = "Test" act.description = "Desc" act.purposes = ["Zweck"] 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 = "hr" act.systems = [] act.deployment_model = "cloud" act.data_sources = [] act.data_flows = [] act.protection_level = "HIGH" act.dpia_required = False act.structured_toms = {} act.status = "DRAFT" act.responsible = "Max" act.owner = "Max" act.last_reviewed_at = None act.next_review_at = None act.created_by = "system" act.dsfa_id = None act.purpose_refs = ["PAYROLL"] act.legal_basis_refs = ["BDSG_26"] act.data_subject_refs = ["EMPLOYEES"] act.data_category_refs = ["NAME"] act.recipient_refs = ["INTERNAL_HR"] act.retention_rule_ref = "HGB_257_10Y" act.transfer_mechanism_refs = None act.tom_refs = ["AC_RBAC"] act.source_template_id = "hr-test" act.risk_score = 3 act.linked_loeschfristen_ids = None act.linked_tom_measure_ids = None act.art30_completeness = {"score": 90, "missing": ["responsible"]} act.created_at = datetime.utcnow() act.updated_at = None resp = _activity_to_response(act) assert resp.purpose_refs == ["PAYROLL"] assert resp.retention_rule_ref == "HGB_257_10Y" assert resp.source_template_id == "hr-test" assert resp.art30_completeness["score"] == 90