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
- 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>
581 lines
21 KiB
Python
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"
|
|
)
|