"""Tests for policy template types (Migration 054) — 29 policy templates.""" import pytest from unittest.mock import MagicMock from fastapi.testclient import TestClient from fastapi import FastAPI from datetime import datetime from compliance.api.legal_template_routes import ( VALID_DOCUMENT_TYPES, VALID_STATUSES, router, ) from compliance.api.db_utils import row_to_dict as _row_to_dict from classroom_engine.database import get_db from compliance.api.tenant_utils import get_tenant_id DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" # ============================================================================= # Test App Setup # ============================================================================= app = FastAPI() app.include_router(router) mock_db = MagicMock() def override_get_db(): yield mock_db def override_tenant(): return DEFAULT_TENANT_ID app.dependency_overrides[get_db] = override_get_db app.dependency_overrides[get_tenant_id] = override_tenant client = TestClient(app) # ============================================================================= # Policy type constants (grouped by category) # ============================================================================= IT_SECURITY_POLICIES = [ "information_security_policy", "access_control_policy", "password_policy", "encryption_policy", "logging_policy", "backup_policy", "incident_response_policy", "change_management_policy", "patch_management_policy", "asset_management_policy", "cloud_security_policy", "devsecops_policy", "secrets_management_policy", "vulnerability_management_policy", ] DATA_POLICIES = [ "data_protection_policy", "data_classification_policy", "data_retention_policy", "data_transfer_policy", "privacy_incident_policy", ] PERSONNEL_POLICIES = [ "employee_security_policy", "security_awareness_policy", "remote_work_policy", "offboarding_policy", ] VENDOR_POLICIES = [ "vendor_risk_management_policy", "third_party_security_policy", "supplier_security_policy", ] BCM_POLICIES = [ "business_continuity_policy", "disaster_recovery_policy", "crisis_management_policy", ] ALL_POLICY_TYPES = ( IT_SECURITY_POLICIES + DATA_POLICIES + PERSONNEL_POLICIES + VENDOR_POLICIES + BCM_POLICIES ) # ============================================================================= # Helpers # ============================================================================= def make_policy_row(doc_type, title="Test Policy", content="# Test", **overrides): data = { "id": "policy-001", "tenant_id": DEFAULT_TENANT_ID, "document_type": doc_type, "title": title, "description": f"Test {doc_type}", "content": content, "placeholders": ["{{COMPANY_NAME}}", "{{SECURITY_OFFICER}}", "{{VERSION}}", "{{DATE}}"], "language": "de", "jurisdiction": "DE", "status": "published", "license_id": "mit", "license_name": "MIT License", "source_name": "BreakPilot Compliance", "attribution_required": False, "is_complete_document": True, "version": "1.0.0", "source_url": None, "source_repo": None, "source_file_path": None, "source_retrieved_at": None, "attribution_text": None, "inspiration_sources": [], "created_at": datetime(2026, 3, 14), "updated_at": datetime(2026, 3, 14), } data.update(overrides) row = MagicMock() row._mapping = data return row # ============================================================================= # TestPolicyTypeValidation # ============================================================================= class TestPolicyTypeValidation: """Verify all 29 policy types are accepted by VALID_DOCUMENT_TYPES.""" def test_all_29_policy_types_present(self): """All 29 policy types from Migration 054 are in VALID_DOCUMENT_TYPES.""" for doc_type in ALL_POLICY_TYPES: assert doc_type in VALID_DOCUMENT_TYPES, ( f"Policy type '{doc_type}' missing from VALID_DOCUMENT_TYPES" ) def test_policy_count(self): """There are exactly 29 policy template types.""" assert len(ALL_POLICY_TYPES) == 29 def test_it_security_policy_count(self): """IT Security category has 14 policy types.""" assert len(IT_SECURITY_POLICIES) == 14 def test_data_policy_count(self): """Data category has 5 policy types.""" assert len(DATA_POLICIES) == 5 def test_personnel_policy_count(self): """Personnel category has 4 policy types.""" assert len(PERSONNEL_POLICIES) == 4 def test_vendor_policy_count(self): """Vendor/Supply Chain category has 3 policy types.""" assert len(VENDOR_POLICIES) == 3 def test_bcm_policy_count(self): """BCM category has 3 policy types.""" assert len(BCM_POLICIES) == 3 def test_total_valid_types_count(self): """VALID_DOCUMENT_TYPES has 52 entries total (16 original + 7 security + 29 policies).""" assert len(VALID_DOCUMENT_TYPES) == 52 def test_no_duplicate_policy_types(self): """No duplicate entries in the policy type lists.""" assert len(ALL_POLICY_TYPES) == len(set(ALL_POLICY_TYPES)) def test_policies_distinct_from_security_concepts(self): """Policy types are distinct from security concept types (Migration 051).""" security_concepts = [ "it_security_concept", "data_protection_concept", "backup_recovery_concept", "logging_concept", "incident_response_plan", "access_control_concept", "risk_management_concept", ] for policy_type in ALL_POLICY_TYPES: assert policy_type not in security_concepts, ( f"Policy type '{policy_type}' clashes with security concept" ) # ============================================================================= # TestPolicyTemplateCreation # ============================================================================= class TestPolicyTemplateCreation: """Test creating policy templates via API.""" def setup_method(self): mock_db.reset_mock() def test_create_information_security_policy(self): """POST /legal-templates accepts information_security_policy.""" row = make_policy_row("information_security_policy", "Informationssicherheits-Richtlinie") mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": "information_security_policy", "title": "Informationssicherheits-Richtlinie", "content": "# Informationssicherheits-Richtlinie\n\n## 1. Zweck", }) assert resp.status_code == 201 def test_create_data_protection_policy(self): """POST /legal-templates accepts data_protection_policy.""" row = make_policy_row("data_protection_policy", "Datenschutz-Richtlinie") mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": "data_protection_policy", "title": "Datenschutz-Richtlinie", "content": "# Datenschutz-Richtlinie", }) assert resp.status_code == 201 def test_create_business_continuity_policy(self): """POST /legal-templates accepts business_continuity_policy.""" row = make_policy_row("business_continuity_policy", "Business-Continuity-Richtlinie") mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": "business_continuity_policy", "title": "Business-Continuity-Richtlinie", "content": "# Business-Continuity-Richtlinie", }) assert resp.status_code == 201 def test_create_vendor_risk_management_policy(self): """POST /legal-templates accepts vendor_risk_management_policy.""" row = make_policy_row("vendor_risk_management_policy", "Lieferanten-Risikomanagement") mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": "vendor_risk_management_policy", "title": "Lieferanten-Risikomanagement-Richtlinie", "content": "# Lieferanten-Risikomanagement", }) assert resp.status_code == 201 def test_create_employee_security_policy(self): """POST /legal-templates accepts employee_security_policy.""" row = make_policy_row("employee_security_policy", "Mitarbeiter-Sicherheitsrichtlinie") mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": "employee_security_policy", "title": "Mitarbeiter-Sicherheitsrichtlinie", "content": "# Mitarbeiter-Sicherheitsrichtlinie", }) assert resp.status_code == 201 @pytest.mark.parametrize("doc_type", ALL_POLICY_TYPES) def test_all_policy_types_accepted_by_api(self, doc_type): """POST /legal-templates accepts every policy type (parametrized).""" row = make_policy_row(doc_type) mock_db.execute.return_value.fetchone.return_value = row mock_db.commit = MagicMock() resp = client.post("/legal-templates", json={ "document_type": doc_type, "title": f"Test {doc_type}", "content": f"# {doc_type}", }) assert resp.status_code == 201, ( f"Expected 201 for {doc_type}, got {resp.status_code}: {resp.text}" ) # ============================================================================= # TestPolicyTemplateFilter # ============================================================================= class TestPolicyTemplateFilter: """Verify filtering templates by policy document types.""" def setup_method(self): mock_db.reset_mock() @pytest.mark.parametrize("doc_type", [ "information_security_policy", "data_protection_policy", "employee_security_policy", "vendor_risk_management_policy", "business_continuity_policy", ]) def test_filter_by_policy_type(self, doc_type): """GET /legal-templates?document_type={policy} returns 200.""" count_mock = MagicMock() count_mock.__getitem__ = lambda self, i: 1 first_call = MagicMock() first_call.fetchone.return_value = count_mock second_call = MagicMock() second_call.fetchall.return_value = [make_policy_row(doc_type)] mock_db.execute.side_effect = [first_call, second_call] resp = client.get(f"/legal-templates?document_type={doc_type}") assert resp.status_code == 200 data = resp.json() assert "templates" in data # ============================================================================= # TestPolicyTemplatePlaceholders # ============================================================================= class TestPolicyTemplatePlaceholders: """Verify placeholder structure for policy templates.""" def test_information_security_policy_placeholders(self): """Information security policy has standard placeholders.""" row = make_policy_row( "information_security_policy", placeholders=[ "{{COMPANY_NAME}}", "{{SECURITY_OFFICER}}", "{{VERSION}}", "{{DATE}}", "{{SCOPE_DESCRIPTION}}", "{{GF_NAME}}", ], ) result = _row_to_dict(row) assert "{{COMPANY_NAME}}" in result["placeholders"] assert "{{SECURITY_OFFICER}}" in result["placeholders"] assert "{{GF_NAME}}" in result["placeholders"] def test_data_protection_policy_placeholders(self): """Data protection policy has DSB and DPO placeholders.""" row = make_policy_row( "data_protection_policy", placeholders=[ "{{COMPANY_NAME}}", "{{DSB_NAME}}", "{{DSB_EMAIL}}", "{{VERSION}}", "{{DATE}}", "{{GF_NAME}}", "{{SCOPE_DESCRIPTION}}", ], ) result = _row_to_dict(row) assert "{{DSB_NAME}}" in result["placeholders"] assert "{{DSB_EMAIL}}" in result["placeholders"] def test_password_policy_placeholders(self): """Password policy has complexity-related placeholders.""" row = make_policy_row( "password_policy", placeholders=[ "{{COMPANY_NAME}}", "{{SECURITY_OFFICER}}", "{{VERSION}}", "{{DATE}}", "{{MIN_PASSWORD_LENGTH}}", "{{MAX_AGE_DAYS}}", "{{HISTORY_COUNT}}", "{{GF_NAME}}", ], ) result = _row_to_dict(row) assert "{{MIN_PASSWORD_LENGTH}}" in result["placeholders"] assert "{{MAX_AGE_DAYS}}" in result["placeholders"] def test_backup_policy_placeholders(self): """Backup policy has retention-related placeholders.""" row = make_policy_row( "backup_policy", placeholders=[ "{{COMPANY_NAME}}", "{{SECURITY_OFFICER}}", "{{VERSION}}", "{{DATE}}", "{{RPO_HOURS}}", "{{RTO_HOURS}}", "{{BACKUP_RETENTION_DAYS}}", "{{GF_NAME}}", ], ) result = _row_to_dict(row) assert "{{RPO_HOURS}}" in result["placeholders"] assert "{{RTO_HOURS}}" in result["placeholders"] # ============================================================================= # TestPolicyTemplateStructure # ============================================================================= class TestPolicyTemplateStructure: """Validate structural aspects of policy templates.""" def test_policy_uses_mit_license(self): """Policy templates use MIT license.""" row = make_policy_row("information_security_policy") result = _row_to_dict(row) assert result["license_id"] == "mit" assert result["license_name"] == "MIT License" assert result["attribution_required"] is False def test_policy_language_de(self): """Policy templates default to German language.""" row = make_policy_row("access_control_policy") result = _row_to_dict(row) assert result["language"] == "de" assert result["jurisdiction"] == "DE" def test_policy_is_complete_document(self): """Policy templates are complete documents.""" row = make_policy_row("encryption_policy") result = _row_to_dict(row) assert result["is_complete_document"] is True def test_policy_default_status_published(self): """Policy templates default to published status.""" row = make_policy_row("logging_policy") result = _row_to_dict(row) assert result["status"] == "published" def test_policy_row_to_dict_datetime(self): """_row_to_dict converts datetime for policy rows.""" row = make_policy_row("patch_management_policy") result = _row_to_dict(row) assert result["created_at"] == "2026-03-14T00:00:00" def test_policy_source_name(self): """Policy templates have BreakPilot Compliance as source.""" row = make_policy_row("cloud_security_policy") result = _row_to_dict(row) assert result["source_name"] == "BreakPilot Compliance" # ============================================================================= # TestPolicyTemplateRejection # ============================================================================= class TestPolicyTemplateRejection: """Verify invalid policy types are rejected.""" def setup_method(self): mock_db.reset_mock() def test_reject_fake_policy_type(self): """POST /legal-templates rejects non-existent policy type.""" resp = client.post("/legal-templates", json={ "document_type": "fake_security_policy", "title": "Fake Policy", "content": "# Fake", }) assert resp.status_code == 400 assert "Invalid document_type" in resp.json()["detail"] def test_reject_policy_with_typo(self): """POST /legal-templates rejects misspelled policy type.""" resp = client.post("/legal-templates", json={ "document_type": "informaton_security_policy", "title": "Typo Policy", "content": "# Typo", }) assert resp.status_code == 400 def test_reject_policy_with_invalid_status(self): """POST /legal-templates rejects invalid status for policy.""" resp = client.post("/legal-templates", json={ "document_type": "password_policy", "title": "Password Policy", "content": "# Password", "status": "active", }) assert resp.status_code == 400 # ============================================================================= # TestPolicySeedScript # ============================================================================= class TestPolicySeedScript: """Validate the seed_policy_templates.py script structure.""" def test_seed_script_exists(self): """Seed script file exists.""" import os path = os.path.join( os.path.dirname(__file__), "..", "scripts", "seed_policy_templates.py" ) assert os.path.exists(path), "seed_policy_templates.py not found" def test_seed_script_importable(self): """Seed script can be parsed without errors.""" import importlib.util import os path = os.path.join( os.path.dirname(__file__), "..", "scripts", "seed_policy_templates.py" ) spec = importlib.util.spec_from_file_location("seed_policy_templates", path) mod = importlib.util.module_from_spec(spec) # Don't execute main() — just verify the module parses # We do this by checking TEMPLATES is defined try: spec.loader.exec_module(mod) except SystemExit: pass # Script may call sys.exit except Exception: pass # Network calls may fail in test env # Module should define TEMPLATES list assert hasattr(mod, "TEMPLATES"), "TEMPLATES list not found in seed script" assert len(mod.TEMPLATES) == 29, f"Expected 29 templates, got {len(mod.TEMPLATES)}" def test_seed_templates_have_required_fields(self): """Each seed template has document_type, title, description, content, placeholders.""" import importlib.util import os path = os.path.join( os.path.dirname(__file__), "..", "scripts", "seed_policy_templates.py" ) spec = importlib.util.spec_from_file_location("seed_policy_templates", path) mod = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(mod) except Exception: pass required_fields = {"document_type", "title", "description", "content", "placeholders"} for tmpl in mod.TEMPLATES: for field in required_fields: assert field in tmpl, ( f"Template '{tmpl.get('document_type', '?')}' missing field '{field}'" ) def test_seed_templates_use_valid_types(self): """All seed template document_types are in VALID_DOCUMENT_TYPES.""" import importlib.util import os path = os.path.join( os.path.dirname(__file__), "..", "scripts", "seed_policy_templates.py" ) spec = importlib.util.spec_from_file_location("seed_policy_templates", path) mod = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(mod) except Exception: pass for tmpl in mod.TEMPLATES: assert tmpl["document_type"] in VALID_DOCUMENT_TYPES, ( f"Seed type '{tmpl['document_type']}' not in VALID_DOCUMENT_TYPES" ) def test_seed_templates_have_german_content(self): """All seed templates have German content (contain common German words).""" import importlib.util import os path = os.path.join( os.path.dirname(__file__), "..", "scripts", "seed_policy_templates.py" ) spec = importlib.util.spec_from_file_location("seed_policy_templates", path) mod = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(mod) except Exception: pass german_markers = ["Richtlinie", "Zweck", "Geltungsbereich", "Verantwortlich"] for tmpl in mod.TEMPLATES: content = tmpl["content"] has_german = any(marker in content for marker in german_markers) assert has_german, ( f"Template '{tmpl['document_type']}' content appears not to be German" )