Files
breakpilot-compliance/backend-compliance/tests/test_legal_template_routes.py
Benjamin Admin f909182632
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Legal Templates Service — eigene Vorlagen für Dokumentengenerator
Implementiert MIT-lizenzierte DSGVO-Templates (DSE, Impressum, AGB) in
der eigenen PostgreSQL-Datenbank statt KLAUSUR_SERVICE-Abhängigkeit.

- Migration 018: compliance_legal_templates Tabelle + 3 Seed-Templates
- Routes: GET/POST/PUT/DELETE /legal-templates + /status + /sources
- Registriert im bestehenden compliance catch-all Proxy (kein neuer Proxy)
- searchTemplates.ts: eigenes Backend als Primary, RAG bleibt Fallback
- ServiceMode-Banner: KLAUSUR_SERVICE-Referenz entfernt
- Tests: 25 Python + 3 Vitest — alle grün

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 23:12:07 +01:00

356 lines
14 KiB
Python

"""Tests for Legal Template routes and schemas (legal_template_routes.py)."""
import json
import pytest
from datetime import datetime
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from fastapi import FastAPI
from compliance.api.legal_template_routes import (
LegalTemplateCreate,
LegalTemplateUpdate,
_row_to_dict,
_get_tenant_id,
DEFAULT_TENANT_ID,
VALID_DOCUMENT_TYPES,
VALID_STATUSES,
router,
)
from classroom_engine.database import get_db
app = FastAPI()
app.include_router(router)
DEFAULT_TENANT = DEFAULT_TENANT_ID
TEMPLATE_ID = "ffffffff-0001-0001-0001-000000000001"
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"
DSE_PLACEHOLDERS = [
"{{COMPANY_NAME}}", "{{COMPANY_ADDRESS}}", "{{CONTACT_EMAIL}}", "{{VERSION_DATE}}"
]
# =============================================================================
# Helpers
# =============================================================================
def make_template_row(overrides=None):
data = {
"id": TEMPLATE_ID,
"tenant_id": DEFAULT_TENANT,
"document_type": "privacy_policy",
"title": "Datenschutzerklärung (DSGVO-konform)",
"description": "Vollständige DSE gemäß DSGVO Art. 13/14",
"content": "# Datenschutzerklärung\n\n## 1. Verantwortlicher\n\n{{COMPANY_NAME}}",
"placeholders": DSE_PLACEHOLDERS,
"language": "de",
"jurisdiction": "DE",
"license_id": "mit",
"license_name": "MIT License",
"source_name": "BreakPilot Compliance",
"attribution_required": False,
"is_complete_document": True,
"version": "1.0.0",
"status": "published",
"created_at": datetime(2024, 1, 1),
"updated_at": datetime(2024, 1, 1),
}
if overrides:
data.update(overrides)
row = MagicMock()
row._mapping = data
return row
def make_db_mock():
db = MagicMock()
return db
# =============================================================================
# TestLegalTemplateSchemas
# =============================================================================
class TestLegalTemplateSchemas:
def test_create_schema_defaults(self):
"""LegalTemplateCreate sets sensible defaults."""
payload = LegalTemplateCreate(
document_type="privacy_policy",
title="Test DSE",
content="# Test"
)
assert payload.language == "de"
assert payload.jurisdiction == "DE"
assert payload.license_id == "mit"
assert payload.license_name == "MIT License"
assert payload.source_name == "BreakPilot Compliance"
assert payload.attribution_required is False
assert payload.is_complete_document is True
assert payload.version == "1.0.0"
assert payload.status == "published"
def test_create_schema_with_placeholders(self):
"""LegalTemplateCreate accepts placeholder list."""
payload = LegalTemplateCreate(
document_type="impressum",
title="Impressum",
content="# Impressum\n{{COMPANY_NAME}}",
placeholders=["{{COMPANY_NAME}}", "{{CEO_NAME}}"]
)
assert len(payload.placeholders) == 2
assert "{{COMPANY_NAME}}" in payload.placeholders
def test_update_schema_all_optional(self):
"""LegalTemplateUpdate: all fields optional."""
payload = LegalTemplateUpdate()
d = payload.model_dump(exclude_unset=True)
assert d == {}
def test_update_schema_partial(self):
"""LegalTemplateUpdate partial: only set fields serialized."""
payload = LegalTemplateUpdate(status="archived", title="Neue DSE")
d = payload.model_dump(exclude_unset=True)
assert d == {"status": "archived", "title": "Neue DSE"}
def test_valid_document_types_constant(self):
"""VALID_DOCUMENT_TYPES contains the 3 expected types."""
assert "privacy_policy" in VALID_DOCUMENT_TYPES
assert "terms_of_service" in VALID_DOCUMENT_TYPES
assert "impressum" in VALID_DOCUMENT_TYPES
assert len(VALID_DOCUMENT_TYPES) == 3
def test_valid_statuses_constant(self):
"""VALID_STATUSES contains expected values."""
assert "published" in VALID_STATUSES
assert "draft" in VALID_STATUSES
assert "archived" in VALID_STATUSES
# =============================================================================
# TestLegalTemplateDB
# =============================================================================
class TestLegalTemplateDB:
def test_row_to_dict_converts_datetime(self):
"""_row_to_dict converts datetime to ISO string."""
row = make_template_row()
result = _row_to_dict(row)
assert result["created_at"] == "2024-01-01T00:00:00"
assert result["updated_at"] == "2024-01-01T00:00:00"
def test_row_to_dict_preserves_strings(self):
"""_row_to_dict preserves string fields unchanged."""
row = make_template_row()
result = _row_to_dict(row)
assert result["title"] == "Datenschutzerklärung (DSGVO-konform)"
assert result["license_id"] == "mit"
assert result["document_type"] == "privacy_policy"
def test_row_to_dict_preserves_list(self):
"""_row_to_dict preserves list fields (placeholders)."""
row = make_template_row()
result = _row_to_dict(row)
assert isinstance(result["placeholders"], list)
assert "{{COMPANY_NAME}}" in result["placeholders"]
def test_get_tenant_id_default(self):
"""_get_tenant_id returns default when no header provided."""
result = _get_tenant_id(None)
assert result == DEFAULT_TENANT_ID
def test_get_tenant_id_valid_uuid(self):
"""_get_tenant_id returns provided UUID when valid."""
custom_uuid = "12345678-1234-1234-1234-123456789abc"
result = _get_tenant_id(custom_uuid)
assert result == custom_uuid
def test_get_tenant_id_invalid_uuid(self):
"""_get_tenant_id falls back to default for invalid UUID."""
result = _get_tenant_id("not-a-uuid")
assert result == DEFAULT_TENANT_ID
# =============================================================================
# TestLegalTemplateSearch
# =============================================================================
class TestLegalTemplateSearch:
def setup_method(self):
self.db = make_db_mock()
app.dependency_overrides[get_db] = lambda: self.db
self.client = TestClient(app)
def teardown_method(self):
app.dependency_overrides.clear()
def _setup_list(self, rows, total=None):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: total if total is not None else len(rows)
self.db.execute.return_value.fetchone.side_effect = [count_row]
self.db.execute.return_value.fetchall.return_value = rows
def test_list_returns_200(self):
"""GET /legal-templates returns 200 with templates list."""
rows = [make_template_row()]
count_mock = MagicMock()
count_mock.__getitem__ = lambda self, i: 1
execute_results = []
first_call = MagicMock()
first_call.fetchone.return_value = count_mock
second_call = MagicMock()
second_call.fetchall.return_value = rows
self.db.execute.side_effect = [first_call, second_call]
res = self.client.get("/legal-templates")
assert res.status_code == 200
data = res.json()
assert "templates" in data
assert "total" in data
def test_list_filter_by_document_type(self):
"""GET /legal-templates?document_type=impressum passes filter."""
count_mock = MagicMock()
count_mock.__getitem__ = lambda self, i: 0
first_call = MagicMock()
first_call.fetchone.return_value = count_mock
second_call = MagicMock()
second_call.fetchall.return_value = []
self.db.execute.side_effect = [first_call, second_call]
res = self.client.get("/legal-templates?document_type=impressum")
assert res.status_code == 200
# Verify the SQL call used document_type filter
call_args = self.db.execute.call_args_list[0]
sql_str = str(call_args[0][0])
assert "document_type" in sql_str
def test_create_invalid_document_type_returns_400(self):
"""POST /legal-templates with invalid document_type returns 400."""
res = self.client.post("/legal-templates", json={
"document_type": "unknown_type",
"title": "Test",
"content": "# Test"
})
assert res.status_code == 400
assert "document_type" in res.json()["detail"]
def test_create_invalid_status_returns_400(self):
"""POST /legal-templates with invalid status returns 400."""
res = self.client.post("/legal-templates", json={
"document_type": "privacy_policy",
"title": "Test",
"content": "# Test",
"status": "invalid_status"
})
assert res.status_code == 400
def test_get_nonexistent_returns_404(self):
"""GET /legal-templates/{id} for unknown ID returns 404."""
self.db.execute.return_value.fetchone.return_value = None
res = self.client.get(f"/legal-templates/{UNKNOWN_ID}")
assert res.status_code == 404
def test_update_no_fields_returns_400(self):
"""PUT /legal-templates/{id} with empty body returns 400."""
res = self.client.put(f"/legal-templates/{TEMPLATE_ID}", json={})
assert res.status_code == 400
assert "No fields" in res.json()["detail"]
def test_delete_nonexistent_returns_404(self):
"""DELETE /legal-templates/{id} for unknown ID returns 404."""
result_mock = MagicMock()
result_mock.rowcount = 0
self.db.execute.return_value = result_mock
res = self.client.delete(f"/legal-templates/{UNKNOWN_ID}")
assert res.status_code == 404
def test_status_endpoint_returns_200(self):
"""GET /legal-templates/status returns count structure."""
row = MagicMock()
row._mapping = {
"total": 3, "published": 3, "draft": 0, "archived": 0,
"privacy_policy": 1, "terms_of_service": 1, "impressum": 1
}
self.db.execute.return_value.fetchone.return_value = row
res = self.client.get("/legal-templates/status")
assert res.status_code == 200
data = res.json()
assert data["total"] == 3
assert "by_type" in data
assert "by_status" in data
def test_sources_endpoint_returns_list(self):
"""GET /legal-templates/sources returns sources list."""
self.db.execute.return_value.fetchall.return_value = [
("BreakPilot Compliance",),
]
res = self.client.get("/legal-templates/sources")
assert res.status_code == 200
data = res.json()
assert "sources" in data
assert "BreakPilot Compliance" in data["sources"]
# =============================================================================
# TestLegalTemplateSeed
# =============================================================================
class TestLegalTemplateSeed:
"""Validate that seed template structures are correct."""
def test_dse_placeholders_present(self):
"""DSE template row has expected placeholder tokens."""
row = make_template_row({
"document_type": "privacy_policy",
"placeholders": [
"{{COMPANY_NAME}}", "{{COMPANY_ADDRESS}}", "{{COMPANY_CITY}}",
"{{CONTACT_EMAIL}}", "{{DPO_NAME}}", "{{DPO_EMAIL}}",
"{{SUPERVISORY_AUTHORITY}}", "{{VERSION_DATE}}"
]
})
result = _row_to_dict(row)
assert "{{COMPANY_NAME}}" in result["placeholders"]
assert "{{CONTACT_EMAIL}}" in result["placeholders"]
assert "{{VERSION_DATE}}" in result["placeholders"]
assert len(result["placeholders"]) == 8
def test_impressum_has_ceo_placeholder(self):
"""Impressum template row has CEO_NAME placeholder."""
row = make_template_row({
"document_type": "impressum",
"placeholders": [
"{{COMPANY_NAME}}", "{{COMPANY_LEGAL_FORM}}", "{{COMPANY_ADDRESS}}",
"{{COMPANY_CITY}}", "{{CEO_NAME}}", "{{COMPANY_PHONE}}",
"{{CONTACT_EMAIL}}", "{{WEBSITE_URL}}", "{{REGISTER_SECTION}}", "{{VAT_SECTION}}"
]
})
result = _row_to_dict(row)
assert "{{CEO_NAME}}" in result["placeholders"]
assert "{{WEBSITE_URL}}" in result["placeholders"]
def test_agb_has_pricing_placeholder(self):
"""AGB template row has PRICING_SECTION placeholder."""
row = make_template_row({
"document_type": "terms_of_service",
"placeholders": [
"{{COMPANY_NAME}}", "{{SERVICE_DESCRIPTION}}", "{{PRICING_SECTION}}",
"{{PAYMENT_TERMS}}", "{{COMPANY_CITY}}", "{{PRIVACY_POLICY_URL}}", "{{VERSION_DATE}}"
]
})
result = _row_to_dict(row)
assert "{{PRICING_SECTION}}" in result["placeholders"]
assert "{{PAYMENT_TERMS}}" in result["placeholders"]
assert "{{PRIVACY_POLICY_URL}}" in result["placeholders"]
def test_all_seeds_use_mit_license(self):
"""All seed templates declare MIT license and no attribution required."""
for doc_type in ["privacy_policy", "terms_of_service", "impressum"]:
row = make_template_row({"document_type": doc_type})
result = _row_to_dict(row)
assert result["license_id"] == "mit"
assert result["license_name"] == "MIT License"
assert result["attribution_required"] is False
assert result["is_complete_document"] is True