feat: Dokumentengenerator — Vollständige Vorlage-Bibliothek + Frontend-Redesign
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 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s

- Migration 020: Typ-Renames (data_processing_agreement→dpa, withdrawal_policy→widerruf) + 11 neue MIT-Templates (NDA DE/EN, SLA, AUP, Community Guidelines, Copyright Policy, Cloud Service Agreement, Data Usage Clause, Cookie Banner, AGB, Liability Clause)
- Backend: VALID_DOCUMENT_TYPES auf 16 Typen erweitert; /legal-templates/status nutzt jetzt dynamisches GROUP BY statt Hard-coded Felder
- searchTemplates.ts: loadAllTemplates() für Library-First UX
- page.tsx: Vollständig-Rewrite — Template-Bibliothek (immer sichtbar) mit Kategorie-Pills, Sprache-Toggle, optionaler Suche, Inline-Preview-Expand und Kachel-Grid; Generator-Section erscheint per Scroll wenn Vorlage gewählt
- Tests: 52/52 bestanden, TestLegalTemplateNewTypes (19 neue Tests) + aktualisierte Typ-Checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 10:47:38 +01:00
parent 87dc22500d
commit 7e5047290c
6 changed files with 1098 additions and 691 deletions

View File

@@ -121,19 +121,35 @@ class TestLegalTemplateSchemas:
assert d == {"status": "archived", "title": "Neue DSE"}
def test_valid_document_types_constant(self):
"""VALID_DOCUMENT_TYPES contains all 6 expected types."""
"""VALID_DOCUMENT_TYPES contains all 16 expected types (post-Migration 020)."""
# Original types
assert "privacy_policy" in VALID_DOCUMENT_TYPES
assert "terms_of_service" in VALID_DOCUMENT_TYPES
assert "impressum" in VALID_DOCUMENT_TYPES
assert "data_processing_agreement" in VALID_DOCUMENT_TYPES
assert "withdrawal_policy" in VALID_DOCUMENT_TYPES
assert "cookie_policy" in VALID_DOCUMENT_TYPES
assert len(VALID_DOCUMENT_TYPES) == 6
# Renamed types (Migration 020)
assert "dpa" in VALID_DOCUMENT_TYPES
assert "widerruf" in VALID_DOCUMENT_TYPES
# New types (Migration 020)
assert "nda" in VALID_DOCUMENT_TYPES
assert "sla" in VALID_DOCUMENT_TYPES
assert "acceptable_use" in VALID_DOCUMENT_TYPES
assert "community_guidelines" in VALID_DOCUMENT_TYPES
assert "copyright_policy" in VALID_DOCUMENT_TYPES
assert "cloud_service_agreement" in VALID_DOCUMENT_TYPES
assert "data_usage_clause" in VALID_DOCUMENT_TYPES
assert "cookie_banner" in VALID_DOCUMENT_TYPES
assert "agb" in VALID_DOCUMENT_TYPES
assert "clause" in VALID_DOCUMENT_TYPES
assert len(VALID_DOCUMENT_TYPES) == 16
# Old names must NOT be present after rename
assert "data_processing_agreement" not in VALID_DOCUMENT_TYPES
assert "withdrawal_policy" not in VALID_DOCUMENT_TYPES
def test_create_schema_attribution_fields(self):
"""LegalTemplateCreate accepts attribution fields."""
"""LegalTemplateCreate accepts attribution fields (uses dpa, post-Migration 020)."""
payload = LegalTemplateCreate(
document_type="data_processing_agreement",
document_type="dpa",
title="AVV Test",
content="# AVV\n{{CONTROLLER_NAME}}",
source_url="https://github.com/example/legal",
@@ -302,23 +318,38 @@ class TestLegalTemplateSearch:
assert res.status_code == 404
def test_status_endpoint_returns_200(self):
"""GET /legal-templates/status returns count structure with all 6 types."""
row = MagicMock()
row._mapping = {
"total": 9, "published": 9, "draft": 0, "archived": 0,
"privacy_policy": 2, "terms_of_service": 2, "impressum": 1,
"data_processing_agreement": 2, "withdrawal_policy": 1, "cookie_policy": 1,
}
self.db.execute.return_value.fetchone.return_value = row
"""GET /legal-templates/status returns dynamic count structure."""
# Mock 3 sequential execute calls: total, by_status, by_type
total_mock = MagicMock()
total_mock.__getitem__ = lambda self, i: 20
status_rows = [
MagicMock(**{"__getitem__": lambda s, i: ["published", 18][i]}),
MagicMock(**{"__getitem__": lambda s, i: ["draft", 2][i]}),
]
type_rows = [
MagicMock(**{"__getitem__": lambda s, i: ["privacy_policy", 2][i]}),
MagicMock(**{"__getitem__": lambda s, i: ["dpa", 2][i]}),
MagicMock(**{"__getitem__": lambda s, i: ["nda", 2][i]}),
MagicMock(**{"__getitem__": lambda s, i: ["widerruf", 1][i]}),
]
call1 = MagicMock(); call1.fetchone.return_value = total_mock
call2 = MagicMock(); call2.fetchall.return_value = status_rows
call3 = MagicMock(); call3.fetchall.return_value = type_rows
self.db.execute.side_effect = [call1, call2, call3]
res = self.client.get("/legal-templates/status")
assert res.status_code == 200
data = res.json()
assert data["total"] == 9
assert "total" in data
assert "by_type" in data
assert "by_status" in data
assert "data_processing_agreement" in data["by_type"]
assert "withdrawal_policy" in data["by_type"]
assert "cookie_policy" in data["by_type"]
assert "dpa" in data["by_type"]
assert "nda" in data["by_type"]
# Old names must not appear (they were renamed)
assert "data_processing_agreement" not in data["by_type"]
assert "withdrawal_policy" not in data["by_type"]
def test_sources_endpoint_returns_list(self):
"""GET /legal-templates/sources returns sources list."""
@@ -396,7 +427,7 @@ class TestLegalTemplateSeed:
def test_dpa_template_has_controller_processor_placeholders(self):
"""AVV/DPA template has CONTROLLER_NAME and PROCESSOR_NAME placeholders."""
row = make_template_row({
"document_type": "data_processing_agreement",
"document_type": "dpa",
"placeholders": [
"{{CONTROLLER_NAME}}", "{{CONTROLLER_ADDRESS}}", "{{CONTROLLER_CITY}}",
"{{PROCESSOR_NAME}}", "{{PROCESSOR_ADDRESS}}", "{{PROCESSOR_CITY}}",
@@ -412,7 +443,7 @@ class TestLegalTemplateSeed:
def test_withdrawal_policy_has_public_domain_license(self):
"""Widerrufsbelehrung uses Public Domain license (based on §5 UrhG source)."""
row = make_template_row({
"document_type": "withdrawal_policy",
"document_type": "widerruf",
"license_id": "public_domain",
"license_name": "Public Domain / Gemeinfrei",
"source_url": "https://www.gesetze-im-internet.de/egbgb/anlage_1.html",
@@ -420,7 +451,7 @@ class TestLegalTemplateSeed:
"inspiration_sources": [{"source": "EGBGB Anlage 1", "license": "Public Domain gemäß §5 UrhG"}],
})
result = _row_to_dict(row)
assert result["document_type"] == "withdrawal_policy"
assert result["document_type"] == "widerruf"
assert result["license_id"] == "public_domain"
assert result["source_url"] == "https://www.gesetze-im-internet.de/egbgb/anlage_1.html"
assert result["attribution_text"] is not None
@@ -455,10 +486,201 @@ class TestLegalTemplateSeed:
"""cookie_policy is accepted as valid document_type."""
assert "cookie_policy" in VALID_DOCUMENT_TYPES
def test_withdrawal_policy_type_valid(self):
"""withdrawal_policy is accepted as valid document_type."""
assert "withdrawal_policy" in VALID_DOCUMENT_TYPES
def test_widerruf_type_valid(self):
"""widerruf is accepted as valid document_type (renamed from withdrawal_policy)."""
assert "widerruf" in VALID_DOCUMENT_TYPES
assert "withdrawal_policy" not in VALID_DOCUMENT_TYPES
def test_dpa_type_valid(self):
"""data_processing_agreement is accepted as valid document_type."""
assert "data_processing_agreement" in VALID_DOCUMENT_TYPES
"""dpa is accepted as valid document_type (renamed from data_processing_agreement)."""
assert "dpa" in VALID_DOCUMENT_TYPES
assert "data_processing_agreement" not in VALID_DOCUMENT_TYPES
# =============================================================================
# TestLegalTemplateNewTypes
# =============================================================================
class TestLegalTemplateNewTypes:
"""Validate new document types added in Migration 020."""
def test_all_16_types_present(self):
"""VALID_DOCUMENT_TYPES has exactly 16 entries."""
assert len(VALID_DOCUMENT_TYPES) == 16
def test_new_types_are_valid(self):
"""All Migration 020 new types are accepted."""
new_types = ["nda", "sla", "acceptable_use", "community_guidelines",
"copyright_policy", "cloud_service_agreement",
"data_usage_clause", "cookie_banner", "agb", "clause"]
for t in new_types:
assert t in VALID_DOCUMENT_TYPES, f"Type '{t}' missing from VALID_DOCUMENT_TYPES"
def test_nda_template_has_parties_placeholders(self):
"""NDA template has DISCLOSING_PARTY and RECEIVING_PARTY placeholders."""
row = make_template_row({
"document_type": "nda",
"title": "Geheimhaltungsvereinbarung (NDA) — DE",
"language": "de",
"placeholders": [
"{{DISCLOSING_PARTY}}", "{{RECEIVING_PARTY}}", "{{PURPOSE}}",
"{{EFFECTIVE_DATE}}", "{{DURATION_YEARS}}", "{{PENALTY_AMOUNT}}",
"{{JURISDICTION_CITY}}"
],
})
result = _row_to_dict(row)
assert result["document_type"] == "nda"
assert "{{DISCLOSING_PARTY}}" in result["placeholders"]
assert "{{RECEIVING_PARTY}}" in result["placeholders"]
assert "{{PURPOSE}}" in result["placeholders"]
def test_nda_en_template_is_valid(self):
"""NDA EN template has EU jurisdiction and English language."""
row = make_template_row({
"document_type": "nda",
"title": "Non-Disclosure Agreement (NDA) — EN (EU)",
"language": "en",
"jurisdiction": "EU",
})
result = _row_to_dict(row)
assert result["document_type"] == "nda"
assert result["language"] == "en"
assert result["jurisdiction"] == "EU"
def test_sla_template_has_availability_placeholder(self):
"""SLA template includes AVAILABILITY_PERCENT placeholder."""
row = make_template_row({
"document_type": "sla",
"placeholders": [
"{{SERVICE_PROVIDER}}", "{{CUSTOMER}}", "{{SERVICE_NAME}}",
"{{AVAILABILITY_PERCENT}}", "{{SUPPORT_EMAIL}}", "{{JURISDICTION_CITY}}"
],
})
result = _row_to_dict(row)
assert result["document_type"] == "sla"
assert "{{AVAILABILITY_PERCENT}}" in result["placeholders"]
def test_acceptable_use_type_valid(self):
"""acceptable_use is a valid document type."""
assert "acceptable_use" in VALID_DOCUMENT_TYPES
def test_acceptable_use_template_structure(self):
"""AUP template has required placeholders."""
row = make_template_row({
"document_type": "acceptable_use",
"language": "en",
"jurisdiction": "EU",
"placeholders": [
"{{COMPANY_NAME}}", "{{SERVICE_NAME}}", "{{EFFECTIVE_DATE}}",
"{{ABUSE_EMAIL}}", "{{COMPANY_ADDRESS}}", "{{CONTACT_EMAIL}}"
],
})
result = _row_to_dict(row)
assert result["document_type"] == "acceptable_use"
assert "{{ABUSE_EMAIL}}" in result["placeholders"]
def test_community_guidelines_type_valid(self):
"""community_guidelines is a valid document type."""
assert "community_guidelines" in VALID_DOCUMENT_TYPES
def test_agb_type_valid(self):
"""agb is a valid document type (German AGB, distinct from terms_of_service)."""
assert "agb" in VALID_DOCUMENT_TYPES
def test_agb_template_has_pricing_and_termination(self):
"""AGB template has PRICING_SECTION and TERMINATION_NOTICE_DAYS placeholders."""
row = make_template_row({
"document_type": "agb",
"placeholders": [
"{{COMPANY_NAME}}", "{{COMPANY_ADDRESS}}", "{{VERSION_DATE}}",
"{{SERVICE_NAME}}", "{{SERVICE_DESCRIPTION}}", "{{PRICING_SECTION}}",
"{{PAYMENT_DAYS}}", "{{PRIVACY_POLICY_URL}}",
"{{TERMINATION_NOTICE_DAYS}}", "{{JURISDICTION_CITY}}"
],
})
result = _row_to_dict(row)
assert result["document_type"] == "agb"
assert "{{PRICING_SECTION}}" in result["placeholders"]
assert "{{TERMINATION_NOTICE_DAYS}}" in result["placeholders"]
def test_clause_type_is_modular(self):
"""clause document type can be a non-complete document (is_complete_document=False)."""
row = make_template_row({
"document_type": "clause",
"is_complete_document": False,
"language": "en",
})
result = _row_to_dict(row)
assert result["document_type"] == "clause"
assert result["is_complete_document"] is False
def test_cloud_service_agreement_type_valid(self):
"""cloud_service_agreement is a valid document type."""
assert "cloud_service_agreement" in VALID_DOCUMENT_TYPES
def test_data_usage_clause_is_non_complete(self):
"""data_usage_clause is typically a clause (non-complete document)."""
assert "data_usage_clause" in VALID_DOCUMENT_TYPES
def test_cookie_banner_type_valid(self):
"""cookie_banner is a valid document type."""
assert "cookie_banner" in VALID_DOCUMENT_TYPES
def test_copyright_policy_type_valid(self):
"""copyright_policy is a valid document type."""
assert "copyright_policy" in VALID_DOCUMENT_TYPES
def test_create_nda_with_new_type(self):
"""POST /legal-templates accepts nda as document_type."""
from fastapi.testclient import TestClient
db = MagicMock()
app2 = __import__('fastapi', fromlist=['FastAPI']).FastAPI()
app2.include_router(router)
from classroom_engine.database import get_db
app2.dependency_overrides[get_db] = lambda: db
row = make_template_row({"document_type": "nda", "title": "NDA Test"})
db.execute.return_value.fetchone.return_value = row
db.commit = MagicMock()
client = TestClient(app2)
res = client.post("/legal-templates", json={
"document_type": "nda",
"title": "NDA Test",
"content": "# NDA\n{{DISCLOSING_PARTY}}"
})
assert res.status_code == 201
def test_create_with_old_dpa_name_rejected(self):
"""POST /legal-templates rejects data_processing_agreement (old name)."""
from fastapi.testclient import TestClient
db = MagicMock()
app2 = __import__('fastapi', fromlist=['FastAPI']).FastAPI()
app2.include_router(router)
from classroom_engine.database import get_db
app2.dependency_overrides[get_db] = lambda: db
client = TestClient(app2)
res = client.post("/legal-templates", json={
"document_type": "data_processing_agreement",
"title": "AVV Old Name",
"content": "# AVV"
})
assert res.status_code == 400
def test_create_with_old_withdrawal_policy_name_rejected(self):
"""POST /legal-templates rejects withdrawal_policy (old name)."""
from fastapi.testclient import TestClient
db = MagicMock()
app2 = __import__('fastapi', fromlist=['FastAPI']).FastAPI()
app2.include_router(router)
from classroom_engine.database import get_db
app2.dependency_overrides[get_db] = lambda: db
client = TestClient(app2)
res = client.post("/legal-templates", json={
"document_type": "withdrawal_policy",
"title": "Widerruf Old Name",
"content": "# Widerruf"
})
assert res.status_code == 400