Some checks failed
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) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
392 lines
16 KiB
Python
392 lines
16 KiB
Python
"""Tests for Document Generation (Phase 5).
|
|
|
|
Verifies:
|
|
- Template generators produce correct output from context
|
|
- DSFA template includes AI risk section
|
|
- VVT generates one entry per processing system
|
|
- TOM includes regulatory-specific measures
|
|
- Loeschfristen maps standard periods
|
|
- Obligation includes DSGVO + AI Act + NIS2 when flags set
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from compliance.api.document_templates.dsfa_template import generate_dsfa_draft
|
|
from compliance.api.document_templates.vvt_template import generate_vvt_drafts
|
|
from compliance.api.document_templates.loeschfristen_template import generate_loeschfristen_drafts
|
|
from compliance.api.document_templates.tom_template import generate_tom_drafts
|
|
from compliance.api.document_templates.obligation_template import generate_obligation_drafts
|
|
|
|
|
|
def _make_ctx(**overrides):
|
|
"""Build a realistic template context."""
|
|
base = {
|
|
"company_name": "Acme GmbH",
|
|
"legal_form": "GmbH",
|
|
"industry": "IT",
|
|
"dpo_name": "Max Datenschutz",
|
|
"dpo_email": "dpo@acme.de",
|
|
"supervisory_authority": "LfDI BW",
|
|
"review_cycle_months": 12,
|
|
"subject_to_nis2": False,
|
|
"subject_to_ai_act": False,
|
|
"subject_to_iso27001": False,
|
|
"uses_ai": False,
|
|
"has_ai_systems": False,
|
|
"processing_systems": [],
|
|
"ai_systems": [],
|
|
"ai_use_cases": [],
|
|
"repos": [],
|
|
"document_sources": [],
|
|
"technical_contacts": [],
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
# =============================================================================
|
|
# DSFA Template
|
|
# =============================================================================
|
|
|
|
class TestDSFATemplate:
|
|
def test_basic_draft(self):
|
|
ctx = _make_ctx()
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert "DSFA" in draft["title"]
|
|
assert draft["status"] == "draft"
|
|
assert draft["involves_ai"] is False
|
|
assert draft["risk_level"] == "medium"
|
|
|
|
def test_ai_draft_is_high_risk(self):
|
|
ctx = _make_ctx(
|
|
has_ai_systems=True,
|
|
ai_systems=[{"name": "Chatbot", "purpose": "Support", "risk_category": "limited", "has_human_oversight": True}],
|
|
subject_to_ai_act=True,
|
|
)
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert draft["involves_ai"] is True
|
|
assert draft["risk_level"] == "high"
|
|
assert len(draft["ai_systems_summary"]) == 1
|
|
|
|
def test_includes_dpo(self):
|
|
ctx = _make_ctx(dpo_name="Dr. Privacy")
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert draft["dpo_name"] == "Dr. Privacy"
|
|
assert "Dr. Privacy" in draft["sections"]["section_1"]["content"]
|
|
|
|
|
|
# =============================================================================
|
|
# VVT Template
|
|
# =============================================================================
|
|
|
|
class TestVVTTemplate:
|
|
def test_generates_per_system(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "SAP HR", "vendor": "SAP", "hosting": "cloud", "personal_data_categories": ["Mitarbeiter"]},
|
|
{"name": "Salesforce", "vendor": "Salesforce", "hosting": "us-cloud", "personal_data_categories": ["Kunden"]},
|
|
])
|
|
drafts = generate_vvt_drafts(ctx)
|
|
assert len(drafts) == 2
|
|
assert drafts[0]["vvt_id"] == "VVT-AUTO-001"
|
|
assert "SAP HR" in drafts[0]["name"]
|
|
|
|
def test_us_cloud_adds_third_country(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "AWS", "vendor": "Amazon", "hosting": "us-cloud", "personal_data_categories": []},
|
|
])
|
|
drafts = generate_vvt_drafts(ctx)
|
|
assert len(drafts[0]["third_country_transfers"]) > 0
|
|
|
|
def test_no_systems_no_drafts(self):
|
|
ctx = _make_ctx(processing_systems=[])
|
|
drafts = generate_vvt_drafts(ctx)
|
|
assert len(drafts) == 0
|
|
|
|
|
|
# =============================================================================
|
|
# TOM Template
|
|
# =============================================================================
|
|
|
|
class TestTOMTemplate:
|
|
def test_base_toms(self):
|
|
ctx = _make_ctx()
|
|
drafts = generate_tom_drafts(ctx)
|
|
assert len(drafts) == 8 # Base TOMs only
|
|
categories = {d["category"] for d in drafts}
|
|
assert "Zutrittskontrolle" in categories
|
|
assert "Zugangskontrolle" in categories
|
|
|
|
def test_nis2_adds_cybersecurity(self):
|
|
ctx = _make_ctx(subject_to_nis2=True)
|
|
drafts = generate_tom_drafts(ctx)
|
|
assert len(drafts) > 8
|
|
categories = {d["category"] for d in drafts}
|
|
assert "Cybersicherheit" in categories
|
|
|
|
def test_ai_act_adds_ki_compliance(self):
|
|
ctx = _make_ctx(subject_to_ai_act=True)
|
|
drafts = generate_tom_drafts(ctx)
|
|
categories = {d["category"] for d in drafts}
|
|
assert "KI-Compliance" in categories
|
|
|
|
def test_iso27001_adds_isms(self):
|
|
ctx = _make_ctx(subject_to_iso27001=True)
|
|
drafts = generate_tom_drafts(ctx)
|
|
categories = {d["category"] for d in drafts}
|
|
assert "ISMS" in categories
|
|
|
|
def test_all_flags_combined(self):
|
|
ctx = _make_ctx(subject_to_nis2=True, subject_to_ai_act=True, subject_to_iso27001=True)
|
|
drafts = generate_tom_drafts(ctx)
|
|
# 8 base + 3 NIS2 + 3 ISO + 3 AI = 17
|
|
assert len(drafts) == 17
|
|
|
|
|
|
# =============================================================================
|
|
# Loeschfristen Template
|
|
# =============================================================================
|
|
|
|
class TestLoeschfristenTemplate:
|
|
def test_generates_per_category(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "HR", "personal_data_categories": ["Bankdaten", "Steuer-ID"]},
|
|
{"name": "CRM", "personal_data_categories": ["Kundendaten"]},
|
|
])
|
|
drafts = generate_loeschfristen_drafts(ctx)
|
|
assert len(drafts) == 3
|
|
categories = {d["data_category"] for d in drafts}
|
|
assert "Bankdaten" in categories
|
|
assert "Steuer-ID" in categories
|
|
assert "Kundendaten" in categories
|
|
|
|
def test_standard_periods_applied(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "Payroll", "personal_data_categories": ["Bankdaten"]},
|
|
])
|
|
drafts = generate_loeschfristen_drafts(ctx)
|
|
bankdaten = [d for d in drafts if d["data_category"] == "Bankdaten"][0]
|
|
assert "10 Jahre" in bankdaten["retention_period"]
|
|
assert "HGB" in bankdaten["legal_basis"]
|
|
|
|
def test_unknown_category_defaults(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "Custom", "personal_data_categories": ["Spezialdaten"]},
|
|
])
|
|
drafts = generate_loeschfristen_drafts(ctx)
|
|
assert drafts[0]["retention_period"] == "Noch festzulegen"
|
|
|
|
def test_deduplicates_categories(self):
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "A", "personal_data_categories": ["Bankdaten"]},
|
|
{"name": "B", "personal_data_categories": ["Bankdaten"]},
|
|
])
|
|
drafts = generate_loeschfristen_drafts(ctx)
|
|
assert len(drafts) == 1 # Deduplicated
|
|
|
|
|
|
# =============================================================================
|
|
# Obligation Template
|
|
# =============================================================================
|
|
|
|
class TestObligationTemplate:
|
|
def test_base_dsgvo(self):
|
|
ctx = _make_ctx()
|
|
drafts = generate_obligation_drafts(ctx)
|
|
assert len(drafts) == 8 # Base DSGVO
|
|
titles = {d["title"] for d in drafts}
|
|
assert "Verzeichnis der Verarbeitungstätigkeiten führen" in titles
|
|
|
|
def test_ai_act_obligations(self):
|
|
ctx = _make_ctx(subject_to_ai_act=True)
|
|
drafts = generate_obligation_drafts(ctx)
|
|
assert len(drafts) > 8
|
|
regs = {d["regulation"] for d in drafts}
|
|
assert "EU AI Act" in regs
|
|
|
|
def test_nis2_obligations(self):
|
|
ctx = _make_ctx(subject_to_nis2=True)
|
|
drafts = generate_obligation_drafts(ctx)
|
|
regs = {d["regulation"] for d in drafts}
|
|
assert "NIS2" in regs
|
|
|
|
def test_all_flags(self):
|
|
ctx = _make_ctx(subject_to_nis2=True, subject_to_ai_act=True)
|
|
drafts = generate_obligation_drafts(ctx)
|
|
# 8 DSGVO + 3 AI + 2 NIS2 = 13
|
|
assert len(drafts) == 13
|
|
|
|
|
|
# =============================================================================
|
|
# Route Registration
|
|
# =============================================================================
|
|
|
|
class TestGenerationRouteRegistration:
|
|
def test_routes_registered(self):
|
|
from compliance.api import router
|
|
paths = [r.path for r in router.routes]
|
|
assert any("generation" in p for p in paths)
|
|
|
|
def test_preview_and_apply(self):
|
|
from compliance.api.generation_routes import router
|
|
paths = [r.path for r in router.routes]
|
|
assert any("preview" in p for p in paths)
|
|
assert any("apply" in p for p in paths)
|
|
|
|
|
|
# =============================================================================
|
|
# _generate_for_type dispatcher
|
|
# =============================================================================
|
|
|
|
class TestGenerateForType:
|
|
"""Tests for the _generate_for_type dispatcher function."""
|
|
|
|
def test_dsfa_returns_single_item_list(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx()
|
|
result = _generate_for_type("dsfa", ctx)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 1
|
|
assert "DSFA" in result[0]["title"]
|
|
|
|
def test_vvt_dispatches_correctly(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "HR System", "vendor": "SAP", "hosting": "cloud", "personal_data_categories": ["Mitarbeiter"]},
|
|
])
|
|
result = _generate_for_type("vvt", ctx)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 1
|
|
assert "HR System" in result[0]["name"]
|
|
|
|
def test_tom_dispatches_correctly(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx()
|
|
result = _generate_for_type("tom", ctx)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 8 # Base TOMs
|
|
|
|
def test_loeschfristen_dispatches_correctly(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx(processing_systems=[
|
|
{"name": "Payroll", "personal_data_categories": ["Bankdaten"]},
|
|
])
|
|
result = _generate_for_type("loeschfristen", ctx)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 1
|
|
|
|
def test_obligation_dispatches_correctly(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx()
|
|
result = _generate_for_type("obligation", ctx)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 8 # Base DSGVO obligations
|
|
|
|
def test_invalid_doc_type_raises_value_error(self):
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx()
|
|
with pytest.raises(ValueError, match="Unknown doc_type"):
|
|
_generate_for_type("nonexistent", ctx)
|
|
|
|
|
|
# =============================================================================
|
|
# VALID_DOC_TYPES validation
|
|
# =============================================================================
|
|
|
|
class TestValidDocTypes:
|
|
"""Tests for doc_type validation constants."""
|
|
|
|
def test_valid_doc_types_contains_all_expected(self):
|
|
from compliance.api.generation_routes import VALID_DOC_TYPES
|
|
expected = {"dsfa", "vvt", "tom", "loeschfristen", "obligation"}
|
|
assert VALID_DOC_TYPES == expected
|
|
|
|
def test_invalid_types_not_accepted(self):
|
|
from compliance.api.generation_routes import VALID_DOC_TYPES
|
|
invalid_types = ["dsgvo", "audit", "risk", "consent", "privacy", ""]
|
|
for t in invalid_types:
|
|
assert t not in VALID_DOC_TYPES, f"{t} should not be in VALID_DOC_TYPES"
|
|
|
|
|
|
# =============================================================================
|
|
# Template Context edge cases
|
|
# =============================================================================
|
|
|
|
class TestTemplateContextEdgeCases:
|
|
"""Tests for template context building and edge cases."""
|
|
|
|
def test_empty_company_name_still_generates(self):
|
|
"""Templates should work even with empty company name."""
|
|
ctx = _make_ctx(company_name="")
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert draft["status"] == "draft"
|
|
assert "DSFA" in draft["title"]
|
|
|
|
def test_minimal_context_generates_all_types(self):
|
|
"""All generators should handle a minimal context without crashing."""
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx = _make_ctx()
|
|
for doc_type in ["dsfa", "vvt", "tom", "loeschfristen", "obligation"]:
|
|
result = _generate_for_type(doc_type, ctx)
|
|
assert isinstance(result, list), f"{doc_type} should return a list"
|
|
|
|
def test_context_with_many_processing_systems(self):
|
|
"""Verify generators handle multiple processing systems correctly."""
|
|
systems = [
|
|
{"name": f"System-{i}", "vendor": f"Vendor-{i}", "hosting": "cloud",
|
|
"personal_data_categories": [f"Kategorie-{i}"]}
|
|
for i in range(5)
|
|
]
|
|
ctx = _make_ctx(processing_systems=systems)
|
|
vvt_drafts = generate_vvt_drafts(ctx)
|
|
assert len(vvt_drafts) == 5
|
|
# Verify sequential VVT IDs
|
|
for i, draft in enumerate(vvt_drafts):
|
|
assert draft["vvt_id"] == f"VVT-AUTO-{i+1:03d}"
|
|
|
|
def test_context_with_multiple_ai_systems(self):
|
|
"""DSFA should list all AI systems in summary."""
|
|
ctx = _make_ctx(
|
|
has_ai_systems=True,
|
|
subject_to_ai_act=True,
|
|
ai_systems=[
|
|
{"name": "Chatbot", "purpose": "Support", "risk_category": "limited", "has_human_oversight": True},
|
|
{"name": "Scoring", "purpose": "Credit", "risk_category": "high", "has_human_oversight": False},
|
|
{"name": "OCR", "purpose": "Documents", "risk_category": "minimal", "has_human_oversight": True},
|
|
],
|
|
)
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert len(draft["ai_systems_summary"]) == 3
|
|
assert draft["risk_level"] == "high"
|
|
|
|
def test_context_without_dpo_uses_empty_string(self):
|
|
"""When dpo_name is empty, templates should still work."""
|
|
ctx = _make_ctx(dpo_name="", dpo_email="")
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert draft["dpo_name"] == ""
|
|
# Should still generate valid sections
|
|
assert "section_1" in draft["sections"]
|
|
|
|
def test_all_regulatory_flags_affect_all_generators(self):
|
|
"""When all regulatory flags are set, all generators should produce more output."""
|
|
from compliance.api.generation_routes import _generate_for_type
|
|
ctx_minimal = _make_ctx()
|
|
ctx_full = _make_ctx(
|
|
subject_to_nis2=True,
|
|
subject_to_ai_act=True,
|
|
subject_to_iso27001=True,
|
|
)
|
|
tom_minimal = _generate_for_type("tom", ctx_minimal)
|
|
tom_full = _generate_for_type("tom", ctx_full)
|
|
assert len(tom_full) > len(tom_minimal)
|
|
|
|
obligation_minimal = _generate_for_type("obligation", ctx_minimal)
|
|
obligation_full = _generate_for_type("obligation", ctx_full)
|
|
assert len(obligation_full) > len(obligation_minimal)
|
|
|
|
def test_dsfa_without_ai_has_empty_ai_summary(self):
|
|
"""DSFA without AI systems should have empty ai_systems_summary."""
|
|
ctx = _make_ctx(has_ai_systems=False, ai_systems=[])
|
|
draft = generate_dsfa_draft(ctx)
|
|
assert draft["ai_systems_summary"] == []
|
|
assert draft["involves_ai"] is False
|