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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user