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 41s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 4s
Tests were failing due to stale mock objects after schema extensions: - DSFA: add _mapping property to _DictRow, use proper mock instead of MagicMock - Company Profile: add 6 missing fields (project_id, offering_urls, etc.) - Legal Templates/Policy: update document type count 52→58 - VVT: add 13 missing attributes to activity mock - Legal Documents: align consent test assertions with production behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
693 lines
29 KiB
Python
693 lines
29 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,
|
|
VALID_DOCUMENT_TYPES,
|
|
VALID_STATUSES,
|
|
router,
|
|
)
|
|
from compliance.api.db_utils import row_to_dict as _row_to_dict
|
|
|
|
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
|
|
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),
|
|
# Attribution columns (Migration 019)
|
|
"source_url": None,
|
|
"source_repo": None,
|
|
"source_file_path": None,
|
|
"source_retrieved_at": None,
|
|
"attribution_text": "Self-authored by BreakPilot Compliance (MIT License, 2026-03-04).",
|
|
"inspiration_sources": [{"source": "Self-authored", "license": "MIT"}],
|
|
}
|
|
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 all 58 expected types (Migration 020+051+054+056+073)."""
|
|
# Original types
|
|
assert "privacy_policy" in VALID_DOCUMENT_TYPES
|
|
assert "terms_of_service" in VALID_DOCUMENT_TYPES
|
|
assert "impressum" in VALID_DOCUMENT_TYPES
|
|
assert "cookie_policy" in VALID_DOCUMENT_TYPES
|
|
# 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
|
|
# Security concepts (Migration 051)
|
|
assert "it_security_concept" in VALID_DOCUMENT_TYPES
|
|
assert "data_protection_concept" in VALID_DOCUMENT_TYPES
|
|
assert "backup_recovery_concept" in VALID_DOCUMENT_TYPES
|
|
assert "logging_concept" in VALID_DOCUMENT_TYPES
|
|
assert "incident_response_plan" in VALID_DOCUMENT_TYPES
|
|
assert "access_control_concept" in VALID_DOCUMENT_TYPES
|
|
assert "risk_management_concept" in VALID_DOCUMENT_TYPES
|
|
# Policy templates (Migration 054) — spot check
|
|
assert "information_security_policy" in VALID_DOCUMENT_TYPES
|
|
assert "data_protection_policy" in VALID_DOCUMENT_TYPES
|
|
assert "business_continuity_policy" in VALID_DOCUMENT_TYPES
|
|
# CRA Cybersecurity (Migration 056)
|
|
assert "cybersecurity_policy" in VALID_DOCUMENT_TYPES
|
|
# DSFA template
|
|
assert "dsfa" in VALID_DOCUMENT_TYPES
|
|
# Module document templates (Migration 073)
|
|
assert "vvt_register" in VALID_DOCUMENT_TYPES
|
|
assert "tom_documentation" in VALID_DOCUMENT_TYPES
|
|
assert "loeschkonzept" in VALID_DOCUMENT_TYPES
|
|
assert "pflichtenregister" in VALID_DOCUMENT_TYPES
|
|
# Total: 16 original + 7 security concepts + 29 policies + 1 CRA + 1 DSFA + 4 module docs = 58
|
|
assert len(VALID_DOCUMENT_TYPES) == 58
|
|
# 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 (uses dpa, post-Migration 020)."""
|
|
payload = LegalTemplateCreate(
|
|
document_type="dpa",
|
|
title="AVV Test",
|
|
content="# AVV\n{{CONTROLLER_NAME}}",
|
|
source_url="https://github.com/example/legal",
|
|
source_repo="https://github.com/example/legal",
|
|
source_file_path="dpa-de.md",
|
|
attribution_text="Self-authored MIT.",
|
|
inspiration_sources=[{"source": "test", "license": "MIT"}],
|
|
)
|
|
assert payload.source_url == "https://github.com/example/legal"
|
|
assert payload.attribution_text == "Self-authored MIT."
|
|
assert len(payload.inspiration_sources) == 1
|
|
|
|
def test_update_schema_inspiration_sources(self):
|
|
"""LegalTemplateUpdate accepts inspiration_sources list."""
|
|
payload = LegalTemplateUpdate(
|
|
inspiration_sources=[{"source": "Self-authored", "license": "MIT"}]
|
|
)
|
|
d = payload.model_dump(exclude_unset=True)
|
|
assert "inspiration_sources" in d
|
|
assert d["inspiration_sources"][0]["license"] == "MIT"
|
|
|
|
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"]
|
|
|
|
|
|
# =============================================================================
|
|
# 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 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 "total" in data
|
|
assert "by_type" in data
|
|
assert "by_status" in data
|
|
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."""
|
|
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
|
|
|
|
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": "dpa",
|
|
"placeholders": [
|
|
"{{CONTROLLER_NAME}}", "{{CONTROLLER_ADDRESS}}", "{{CONTROLLER_CITY}}",
|
|
"{{PROCESSOR_NAME}}", "{{PROCESSOR_ADDRESS}}", "{{PROCESSOR_CITY}}",
|
|
"{{SERVICE_DESCRIPTION}}", "{{DATA_CATEGORIES}}", "{{DATA_SUBJECTS}}",
|
|
"{{CONTRACT_DATE}}", "{{SUPERVISORY_AUTHORITY}}"
|
|
]
|
|
})
|
|
result = _row_to_dict(row)
|
|
assert "{{CONTROLLER_NAME}}" in result["placeholders"]
|
|
assert "{{PROCESSOR_NAME}}" in result["placeholders"]
|
|
assert "{{DATA_CATEGORIES}}" in result["placeholders"]
|
|
|
|
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": "widerruf",
|
|
"license_id": "public_domain",
|
|
"license_name": "Public Domain / Gemeinfrei",
|
|
"source_url": "https://www.gesetze-im-internet.de/egbgb/anlage_1.html",
|
|
"attribution_text": "Basiert auf der amtlichen Musterwiderrufsbelehrung.",
|
|
"inspiration_sources": [{"source": "EGBGB Anlage 1", "license": "Public Domain gemäß §5 UrhG"}],
|
|
})
|
|
result = _row_to_dict(row)
|
|
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
|
|
|
|
def test_attribution_text_stored_in_row(self):
|
|
"""attribution_text and inspiration_sources are returned in _row_to_dict."""
|
|
row = make_template_row({
|
|
"attribution_text": "Self-authored by BreakPilot Compliance (MIT License, 2026-03-04).",
|
|
"inspiration_sources": [{"source": "Self-authored", "license": "MIT"}],
|
|
"source_url": None,
|
|
"source_repo": None,
|
|
"source_file_path": None,
|
|
"source_retrieved_at": None,
|
|
})
|
|
result = _row_to_dict(row)
|
|
assert "Self-authored" in result["attribution_text"]
|
|
assert result["inspiration_sources"][0]["license"] == "MIT"
|
|
|
|
def test_english_privacy_policy_eu_jurisdiction(self):
|
|
"""EN Privacy Policy template uses EU jurisdiction."""
|
|
row = make_template_row({
|
|
"document_type": "privacy_policy",
|
|
"language": "en",
|
|
"jurisdiction": "EU",
|
|
"title": "Privacy Policy (GDPR-compliant, EU)",
|
|
})
|
|
result = _row_to_dict(row)
|
|
assert result["language"] == "en"
|
|
assert result["jurisdiction"] == "EU"
|
|
|
|
def test_cookie_policy_type_valid(self):
|
|
"""cookie_policy is accepted as valid document_type."""
|
|
assert "cookie_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):
|
|
"""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_58_types_present(self):
|
|
"""VALID_DOCUMENT_TYPES has exactly 58 entries (16 + 7 security + 29 policies + 1 CRA + 1 DSFA + 4 module docs)."""
|
|
assert len(VALID_DOCUMENT_TYPES) == 58
|
|
|
|
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
|