"""Tests for Document Generation (Phase 5). Verifies: - Template generators produce correct output from context - DSFA template includes AI risk section - VVT generates one entry per processing system - TOM includes regulatory-specific measures - Loeschfristen maps standard periods - Obligation includes DSGVO + AI Act + NIS2 when flags set """ import pytest from compliance.api.document_templates.dsfa_template import generate_dsfa_draft from compliance.api.document_templates.vvt_template import generate_vvt_drafts from compliance.api.document_templates.loeschfristen_template import generate_loeschfristen_drafts from compliance.api.document_templates.tom_template import generate_tom_drafts from compliance.api.document_templates.obligation_template import generate_obligation_drafts def _make_ctx(**overrides): """Build a realistic template context.""" base = { "company_name": "Acme GmbH", "legal_form": "GmbH", "industry": "IT", "dpo_name": "Max Datenschutz", "dpo_email": "dpo@acme.de", "supervisory_authority": "LfDI BW", "review_cycle_months": 12, "subject_to_nis2": False, "subject_to_ai_act": False, "subject_to_iso27001": False, "uses_ai": False, "has_ai_systems": False, "processing_systems": [], "ai_systems": [], "ai_use_cases": [], "repos": [], "document_sources": [], "technical_contacts": [], } base.update(overrides) return base # ============================================================================= # DSFA Template # ============================================================================= class TestDSFATemplate: def test_basic_draft(self): ctx = _make_ctx() draft = generate_dsfa_draft(ctx) assert "DSFA" in draft["title"] assert draft["status"] == "draft" assert draft["involves_ai"] is False assert draft["risk_level"] == "medium" def test_ai_draft_is_high_risk(self): ctx = _make_ctx( has_ai_systems=True, ai_systems=[{"name": "Chatbot", "purpose": "Support", "risk_category": "limited", "has_human_oversight": True}], subject_to_ai_act=True, ) draft = generate_dsfa_draft(ctx) assert draft["involves_ai"] is True assert draft["risk_level"] == "high" assert len(draft["ai_systems_summary"]) == 1 def test_includes_dpo(self): ctx = _make_ctx(dpo_name="Dr. Privacy") draft = generate_dsfa_draft(ctx) assert draft["dpo_name"] == "Dr. Privacy" assert "Dr. Privacy" in draft["sections"]["section_1"]["content"] # ============================================================================= # VVT Template # ============================================================================= class TestVVTTemplate: def test_generates_per_system(self): ctx = _make_ctx(processing_systems=[ {"name": "SAP HR", "vendor": "SAP", "hosting": "cloud", "personal_data_categories": ["Mitarbeiter"]}, {"name": "Salesforce", "vendor": "Salesforce", "hosting": "us-cloud", "personal_data_categories": ["Kunden"]}, ]) drafts = generate_vvt_drafts(ctx) assert len(drafts) == 2 assert drafts[0]["vvt_id"] == "VVT-AUTO-001" assert "SAP HR" in drafts[0]["name"] def test_us_cloud_adds_third_country(self): ctx = _make_ctx(processing_systems=[ {"name": "AWS", "vendor": "Amazon", "hosting": "us-cloud", "personal_data_categories": []}, ]) drafts = generate_vvt_drafts(ctx) assert len(drafts[0]["third_country_transfers"]) > 0 def test_no_systems_no_drafts(self): ctx = _make_ctx(processing_systems=[]) drafts = generate_vvt_drafts(ctx) assert len(drafts) == 0 # ============================================================================= # TOM Template # ============================================================================= class TestTOMTemplate: def test_base_toms(self): ctx = _make_ctx() drafts = generate_tom_drafts(ctx) assert len(drafts) == 8 # Base TOMs only categories = {d["category"] for d in drafts} assert "Zutrittskontrolle" in categories assert "Zugangskontrolle" in categories def test_nis2_adds_cybersecurity(self): ctx = _make_ctx(subject_to_nis2=True) drafts = generate_tom_drafts(ctx) assert len(drafts) > 8 categories = {d["category"] for d in drafts} assert "Cybersicherheit" in categories def test_ai_act_adds_ki_compliance(self): ctx = _make_ctx(subject_to_ai_act=True) drafts = generate_tom_drafts(ctx) categories = {d["category"] for d in drafts} assert "KI-Compliance" in categories def test_iso27001_adds_isms(self): ctx = _make_ctx(subject_to_iso27001=True) drafts = generate_tom_drafts(ctx) categories = {d["category"] for d in drafts} assert "ISMS" in categories def test_all_flags_combined(self): ctx = _make_ctx(subject_to_nis2=True, subject_to_ai_act=True, subject_to_iso27001=True) drafts = generate_tom_drafts(ctx) # 8 base + 3 NIS2 + 3 ISO + 3 AI = 17 assert len(drafts) == 17 # ============================================================================= # Loeschfristen Template # ============================================================================= class TestLoeschfristenTemplate: def test_generates_per_category(self): ctx = _make_ctx(processing_systems=[ {"name": "HR", "personal_data_categories": ["Bankdaten", "Steuer-ID"]}, {"name": "CRM", "personal_data_categories": ["Kundendaten"]}, ]) drafts = generate_loeschfristen_drafts(ctx) assert len(drafts) == 3 categories = {d["data_category"] for d in drafts} assert "Bankdaten" in categories assert "Steuer-ID" in categories assert "Kundendaten" in categories def test_standard_periods_applied(self): ctx = _make_ctx(processing_systems=[ {"name": "Payroll", "personal_data_categories": ["Bankdaten"]}, ]) drafts = generate_loeschfristen_drafts(ctx) bankdaten = [d for d in drafts if d["data_category"] == "Bankdaten"][0] assert "10 Jahre" in bankdaten["retention_period"] assert "HGB" in bankdaten["legal_basis"] def test_unknown_category_defaults(self): ctx = _make_ctx(processing_systems=[ {"name": "Custom", "personal_data_categories": ["Spezialdaten"]}, ]) drafts = generate_loeschfristen_drafts(ctx) assert drafts[0]["retention_period"] == "Noch festzulegen" def test_deduplicates_categories(self): ctx = _make_ctx(processing_systems=[ {"name": "A", "personal_data_categories": ["Bankdaten"]}, {"name": "B", "personal_data_categories": ["Bankdaten"]}, ]) drafts = generate_loeschfristen_drafts(ctx) assert len(drafts) == 1 # Deduplicated # ============================================================================= # Obligation Template # ============================================================================= class TestObligationTemplate: def test_base_dsgvo(self): ctx = _make_ctx() drafts = generate_obligation_drafts(ctx) assert len(drafts) == 8 # Base DSGVO titles = {d["title"] for d in drafts} assert "Verzeichnis der Verarbeitungstätigkeiten führen" in titles def test_ai_act_obligations(self): ctx = _make_ctx(subject_to_ai_act=True) drafts = generate_obligation_drafts(ctx) assert len(drafts) > 8 regs = {d["regulation"] for d in drafts} assert "EU AI Act" in regs def test_nis2_obligations(self): ctx = _make_ctx(subject_to_nis2=True) drafts = generate_obligation_drafts(ctx) regs = {d["regulation"] for d in drafts} assert "NIS2" in regs def test_all_flags(self): ctx = _make_ctx(subject_to_nis2=True, subject_to_ai_act=True) drafts = generate_obligation_drafts(ctx) # 8 DSGVO + 3 AI + 2 NIS2 = 13 assert len(drafts) == 13 # ============================================================================= # Route Registration # ============================================================================= class TestGenerationRouteRegistration: def test_routes_registered(self): from compliance.api import router paths = [r.path for r in router.routes] assert any("generation" in p for p in paths) def test_preview_and_apply(self): from compliance.api.generation_routes import router paths = [r.path for r in router.routes] assert any("preview" in p for p in paths) assert any("apply" in p for p in paths) # ============================================================================= # _generate_for_type dispatcher # ============================================================================= class TestGenerateForType: """Tests for the _generate_for_type dispatcher function.""" def test_dsfa_returns_single_item_list(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx() result = _generate_for_type("dsfa", ctx) assert isinstance(result, list) assert len(result) == 1 assert "DSFA" in result[0]["title"] def test_vvt_dispatches_correctly(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx(processing_systems=[ {"name": "HR System", "vendor": "SAP", "hosting": "cloud", "personal_data_categories": ["Mitarbeiter"]}, ]) result = _generate_for_type("vvt", ctx) assert isinstance(result, list) assert len(result) == 1 assert "HR System" in result[0]["name"] def test_tom_dispatches_correctly(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx() result = _generate_for_type("tom", ctx) assert isinstance(result, list) assert len(result) == 8 # Base TOMs def test_loeschfristen_dispatches_correctly(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx(processing_systems=[ {"name": "Payroll", "personal_data_categories": ["Bankdaten"]}, ]) result = _generate_for_type("loeschfristen", ctx) assert isinstance(result, list) assert len(result) == 1 def test_obligation_dispatches_correctly(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx() result = _generate_for_type("obligation", ctx) assert isinstance(result, list) assert len(result) == 8 # Base DSGVO obligations def test_invalid_doc_type_raises_value_error(self): from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx() with pytest.raises(ValueError, match="Unknown doc_type"): _generate_for_type("nonexistent", ctx) # ============================================================================= # VALID_DOC_TYPES validation # ============================================================================= class TestValidDocTypes: """Tests for doc_type validation constants.""" def test_valid_doc_types_contains_all_expected(self): from compliance.api.generation_routes import VALID_DOC_TYPES expected = {"dsfa", "vvt", "tom", "loeschfristen", "obligation"} assert VALID_DOC_TYPES == expected def test_invalid_types_not_accepted(self): from compliance.api.generation_routes import VALID_DOC_TYPES invalid_types = ["dsgvo", "audit", "risk", "consent", "privacy", ""] for t in invalid_types: assert t not in VALID_DOC_TYPES, f"{t} should not be in VALID_DOC_TYPES" # ============================================================================= # Template Context edge cases # ============================================================================= class TestTemplateContextEdgeCases: """Tests for template context building and edge cases.""" def test_empty_company_name_still_generates(self): """Templates should work even with empty company name.""" ctx = _make_ctx(company_name="") draft = generate_dsfa_draft(ctx) assert draft["status"] == "draft" assert "DSFA" in draft["title"] def test_minimal_context_generates_all_types(self): """All generators should handle a minimal context without crashing.""" from compliance.api.generation_routes import _generate_for_type ctx = _make_ctx() for doc_type in ["dsfa", "vvt", "tom", "loeschfristen", "obligation"]: result = _generate_for_type(doc_type, ctx) assert isinstance(result, list), f"{doc_type} should return a list" def test_context_with_many_processing_systems(self): """Verify generators handle multiple processing systems correctly.""" systems = [ {"name": f"System-{i}", "vendor": f"Vendor-{i}", "hosting": "cloud", "personal_data_categories": [f"Kategorie-{i}"]} for i in range(5) ] ctx = _make_ctx(processing_systems=systems) vvt_drafts = generate_vvt_drafts(ctx) assert len(vvt_drafts) == 5 # Verify sequential VVT IDs for i, draft in enumerate(vvt_drafts): assert draft["vvt_id"] == f"VVT-AUTO-{i+1:03d}" def test_context_with_multiple_ai_systems(self): """DSFA should list all AI systems in summary.""" ctx = _make_ctx( has_ai_systems=True, subject_to_ai_act=True, ai_systems=[ {"name": "Chatbot", "purpose": "Support", "risk_category": "limited", "has_human_oversight": True}, {"name": "Scoring", "purpose": "Credit", "risk_category": "high", "has_human_oversight": False}, {"name": "OCR", "purpose": "Documents", "risk_category": "minimal", "has_human_oversight": True}, ], ) draft = generate_dsfa_draft(ctx) assert len(draft["ai_systems_summary"]) == 3 assert draft["risk_level"] == "high" def test_context_without_dpo_uses_empty_string(self): """When dpo_name is empty, templates should still work.""" ctx = _make_ctx(dpo_name="", dpo_email="") draft = generate_dsfa_draft(ctx) assert draft["dpo_name"] == "" # Should still generate valid sections assert "section_1" in draft["sections"] def test_all_regulatory_flags_affect_all_generators(self): """When all regulatory flags are set, all generators should produce more output.""" from compliance.api.generation_routes import _generate_for_type ctx_minimal = _make_ctx() ctx_full = _make_ctx( subject_to_nis2=True, subject_to_ai_act=True, subject_to_iso27001=True, ) tom_minimal = _generate_for_type("tom", ctx_minimal) tom_full = _generate_for_type("tom", ctx_full) assert len(tom_full) > len(tom_minimal) obligation_minimal = _generate_for_type("obligation", ctx_minimal) obligation_full = _generate_for_type("obligation", ctx_full) assert len(obligation_full) > len(obligation_minimal) def test_dsfa_without_ai_has_empty_ai_summary(self): """DSFA without AI systems should have empty ai_systems_summary.""" ctx = _make_ctx(has_ai_systems=False, ai_systems=[]) draft = generate_dsfa_draft(ctx) assert draft["ai_systems_summary"] == [] assert draft["involves_ai"] is False