Files
breakpilot-compliance/backend-compliance/tests/test_policy_templates.py
Benjamin Admin a9f291ff49
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
test+docs: add policy library tests (67 tests) and MKDocs documentation
- New test_policy_templates.py: 67 tests covering all 29 policy types,
  API creation, filtering, placeholders, seed script validation
- Updated test_legal_template_routes.py: fix type count 16→52
- New MKDocs page policy-bibliothek.md with full template reference
- Updated dokumentengenerierung.md and rechtliche-texte.md with cross-refs
- Added policy-bibliothek to mkdocs.yml navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:50:50 +01:00

581 lines
21 KiB
Python

"""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"
)