feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6)
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 32s
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 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 32s
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 18s
6-Phasen-Implementation fuer cloud-faehiges, mandantenfaehiges Compliance SDK:
Phase 1: Multi-Tenancy Fix
- Shared tenant_utils.py Dependency (UUID-Validierung, kein "default" mehr)
- VVT tenant_id Column + tenant-scoped Queries
- DSFA/Vendor DEFAULT_TENANT_ID von "default" auf UUID migriert
- Migration 035
Phase 2: Stammdaten-Erweiterung
- Company Profile um JSONB-Felder erweitert (processing_systems, ai_systems, technical_contacts)
- Regulierungs-Flags (NIS2, AI Act, ISO 27001)
- GET /template-context Endpoint
- Migration 036
Phase 3: Dokument-Versionierung
- 5 Versions-Tabellen (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Shared versioning_utils.py Helper
- /{id}/versions Endpoints auf allen 5 Dokumenttypen
- Migration 037
Phase 4: Change-Request System
- Zentrale CR-Inbox mit CRUD + Accept/Reject/Edit Workflow
- Regelbasierte CR-Engine (VVT DPIA → DSFA CR, Datenkategorien → Loeschfristen CR)
- Audit-Trail
- Migration 038
Phase 5: Dokumentengenerierung
- 5 Template-Generatoren (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Preview + Apply Endpoints (erzeugt CRs, keine direkten Dokumente)
Phase 6: Frontend-Integration
- Change-Request Inbox Page mit Stats, Filtern, Modals
- VersionHistory Timeline-Komponente
- SDKSidebar CR-Badge (60s Polling)
- Company Profile: 2 neue Wizard-Steps + "Dokumente generieren" CTA
Docs: 5 neue MkDocs-Seiten, CLAUDE.md aktualisiert
Tests: 97 neue Tests (alle bestanden)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
268
backend-compliance/tests/test_company_profile_extend.py
Normal file
268
backend-compliance/tests/test_company_profile_extend.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""Tests for Company Profile extension (Phase 2: Stammdaten).
|
||||
|
||||
Verifies:
|
||||
- New JSONB fields in request/response models
|
||||
- template-context endpoint returns flat dict
|
||||
- Regulatory booleans default correctly
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from compliance.api.company_profile_routes import (
|
||||
CompanyProfileRequest,
|
||||
CompanyProfileResponse,
|
||||
row_to_response,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests — Request Model
|
||||
# =============================================================================
|
||||
|
||||
class TestCompanyProfileRequestExtended:
|
||||
def test_default_new_fields(self):
|
||||
req = CompanyProfileRequest(company_name="Acme GmbH")
|
||||
assert req.repos == []
|
||||
assert req.document_sources == []
|
||||
assert req.processing_systems == []
|
||||
assert req.ai_systems == []
|
||||
assert req.technical_contacts == []
|
||||
assert req.subject_to_nis2 is False
|
||||
assert req.subject_to_ai_act is False
|
||||
assert req.subject_to_iso27001 is False
|
||||
assert req.supervisory_authority is None
|
||||
assert req.review_cycle_months == 12
|
||||
|
||||
def test_full_new_fields(self):
|
||||
req = CompanyProfileRequest(
|
||||
company_name="Test AG",
|
||||
repos=[{"name": "backend", "url": "https://git.example.com/backend", "language": "Python", "has_personal_data": True}],
|
||||
processing_systems=[{"name": "SAP HR", "vendor": "SAP", "hosting": "cloud", "personal_data_categories": ["Mitarbeiter"]}],
|
||||
ai_systems=[{"name": "Chatbot", "purpose": "Kundenservice", "risk_category": "limited", "vendor": "OpenAI", "has_human_oversight": True}],
|
||||
technical_contacts=[{"role": "CISO", "name": "Max Muster", "email": "ciso@example.com"}],
|
||||
subject_to_nis2=True,
|
||||
subject_to_ai_act=True,
|
||||
supervisory_authority="LfDI Baden-Württemberg",
|
||||
review_cycle_months=6,
|
||||
)
|
||||
assert len(req.repos) == 1
|
||||
assert req.repos[0]["language"] == "Python"
|
||||
assert len(req.ai_systems) == 1
|
||||
assert req.subject_to_nis2 is True
|
||||
assert req.review_cycle_months == 6
|
||||
|
||||
def test_serialization_includes_new_fields(self):
|
||||
req = CompanyProfileRequest(company_name="Test")
|
||||
data = req.model_dump()
|
||||
assert "repos" in data
|
||||
assert "processing_systems" in data
|
||||
assert "ai_systems" in data
|
||||
assert "subject_to_nis2" in data
|
||||
assert "review_cycle_months" in data
|
||||
|
||||
def test_backward_compatible(self):
|
||||
"""Old-format requests (without new fields) still work."""
|
||||
req = CompanyProfileRequest(
|
||||
company_name="Legacy Corp",
|
||||
legal_form="GmbH",
|
||||
industry="Manufacturing",
|
||||
)
|
||||
assert req.company_name == "Legacy Corp"
|
||||
assert req.repos == []
|
||||
assert req.subject_to_ai_act is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests — Response Model
|
||||
# =============================================================================
|
||||
|
||||
class TestCompanyProfileResponseExtended:
|
||||
def test_response_includes_new_fields(self):
|
||||
resp = CompanyProfileResponse(
|
||||
id="test-id",
|
||||
tenant_id="test-tenant",
|
||||
company_name="Test",
|
||||
legal_form="GmbH",
|
||||
industry="IT",
|
||||
founded_year=2020,
|
||||
business_model="B2B",
|
||||
offerings=[],
|
||||
company_size="small",
|
||||
employee_count="10-49",
|
||||
annual_revenue="< 2 Mio",
|
||||
headquarters_country="DE",
|
||||
headquarters_city="Berlin",
|
||||
has_international_locations=False,
|
||||
international_countries=[],
|
||||
target_markets=["DE"],
|
||||
primary_jurisdiction="DE",
|
||||
is_data_controller=True,
|
||||
is_data_processor=False,
|
||||
uses_ai=True,
|
||||
ai_use_cases=["chatbot"],
|
||||
dpo_name="DSB",
|
||||
dpo_email="dsb@test.de",
|
||||
legal_contact_name=None,
|
||||
legal_contact_email=None,
|
||||
machine_builder=None,
|
||||
is_complete=True,
|
||||
completed_at="2026-01-01",
|
||||
created_at="2025-12-01",
|
||||
updated_at="2026-01-01",
|
||||
repos=[{"name": "main"}],
|
||||
ai_systems=[{"name": "Bot"}],
|
||||
subject_to_ai_act=True,
|
||||
review_cycle_months=6,
|
||||
)
|
||||
assert resp.repos == [{"name": "main"}]
|
||||
assert resp.ai_systems == [{"name": "Bot"}]
|
||||
assert resp.subject_to_ai_act is True
|
||||
assert resp.review_cycle_months == 6
|
||||
|
||||
def test_response_defaults(self):
|
||||
resp = CompanyProfileResponse(
|
||||
id="x", tenant_id="t", company_name="X", legal_form="GmbH",
|
||||
industry="", founded_year=None, business_model="B2B", offerings=[],
|
||||
company_size="small", employee_count="1-9", annual_revenue="< 2 Mio",
|
||||
headquarters_country="DE", headquarters_city="",
|
||||
has_international_locations=False, international_countries=[],
|
||||
target_markets=["DE"], primary_jurisdiction="DE",
|
||||
is_data_controller=True, is_data_processor=False,
|
||||
uses_ai=False, ai_use_cases=[], dpo_name=None, dpo_email=None,
|
||||
legal_contact_name=None, legal_contact_email=None,
|
||||
machine_builder=None, is_complete=False,
|
||||
completed_at=None, created_at="2026-01-01", updated_at="2026-01-01",
|
||||
)
|
||||
assert resp.repos == []
|
||||
assert resp.processing_systems == []
|
||||
assert resp.subject_to_nis2 is False
|
||||
assert resp.review_cycle_months == 12
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# row_to_response — extended column mapping
|
||||
# =============================================================================
|
||||
|
||||
class TestRowToResponseExtended:
|
||||
def _make_row(self, **overrides):
|
||||
"""Build a 40-element tuple matching the SQL column order."""
|
||||
base = [
|
||||
"uuid-1", # 0: id
|
||||
"tenant-1", # 1: tenant_id
|
||||
"Acme GmbH", # 2: company_name
|
||||
"GmbH", # 3: legal_form
|
||||
"IT", # 4: industry
|
||||
2020, # 5: founded_year
|
||||
"B2B", # 6: business_model
|
||||
["SaaS"], # 7: offerings
|
||||
"medium", # 8: company_size
|
||||
"50-249", # 9: employee_count
|
||||
"2-10 Mio", # 10: annual_revenue
|
||||
"DE", # 11: headquarters_country
|
||||
"München", # 12: headquarters_city
|
||||
False, # 13: has_international_locations
|
||||
[], # 14: international_countries
|
||||
["DE", "AT"], # 15: target_markets
|
||||
"DE", # 16: primary_jurisdiction
|
||||
True, # 17: is_data_controller
|
||||
False, # 18: is_data_processor
|
||||
True, # 19: uses_ai
|
||||
["chatbot"], # 20: ai_use_cases
|
||||
"DSB Person", # 21: dpo_name
|
||||
"dsb@acme.de", # 22: dpo_email
|
||||
None, # 23: legal_contact_name
|
||||
None, # 24: legal_contact_email
|
||||
None, # 25: machine_builder
|
||||
True, # 26: is_complete
|
||||
"2026-01-15", # 27: completed_at
|
||||
"2025-12-01", # 28: created_at
|
||||
"2026-01-15", # 29: updated_at
|
||||
# Phase 2 fields
|
||||
[{"name": "repo1"}], # 30: repos
|
||||
[{"type": "policy", "title": "Privacy Policy"}], # 31: document_sources
|
||||
[{"name": "SAP", "vendor": "SAP"}], # 32: processing_systems
|
||||
[{"name": "Bot", "risk_category": "limited"}], # 33: ai_systems
|
||||
[{"role": "CISO", "name": "Max"}], # 34: technical_contacts
|
||||
True, # 35: subject_to_nis2
|
||||
True, # 36: subject_to_ai_act
|
||||
False, # 37: subject_to_iso27001
|
||||
"LfDI BW", # 38: supervisory_authority
|
||||
6, # 39: review_cycle_months
|
||||
]
|
||||
return tuple(base)
|
||||
|
||||
def test_maps_new_fields(self):
|
||||
row = self._make_row()
|
||||
resp = row_to_response(row)
|
||||
assert resp.repos == [{"name": "repo1"}]
|
||||
assert resp.document_sources[0]["type"] == "policy"
|
||||
assert resp.processing_systems[0]["name"] == "SAP"
|
||||
assert resp.ai_systems[0]["risk_category"] == "limited"
|
||||
assert resp.technical_contacts[0]["role"] == "CISO"
|
||||
assert resp.subject_to_nis2 is True
|
||||
assert resp.subject_to_ai_act is True
|
||||
assert resp.subject_to_iso27001 is False
|
||||
assert resp.supervisory_authority == "LfDI BW"
|
||||
assert resp.review_cycle_months == 6
|
||||
|
||||
def test_null_new_fields_default_gracefully(self):
|
||||
base = list(self._make_row())
|
||||
# Set new fields to None
|
||||
for i in range(30, 40):
|
||||
base[i] = None
|
||||
row = tuple(base)
|
||||
resp = row_to_response(row)
|
||||
assert resp.repos == []
|
||||
assert resp.processing_systems == []
|
||||
assert resp.ai_systems == []
|
||||
assert resp.subject_to_nis2 is False
|
||||
assert resp.supervisory_authority is None
|
||||
assert resp.review_cycle_months == 12
|
||||
|
||||
def test_old_fields_still_work(self):
|
||||
row = self._make_row()
|
||||
resp = row_to_response(row)
|
||||
assert resp.company_name == "Acme GmbH"
|
||||
assert resp.industry == "IT"
|
||||
assert resp.is_complete is True
|
||||
assert resp.dpo_name == "DSB Person"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Template Context Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestTemplateContext:
|
||||
def test_template_context_from_response(self):
|
||||
"""Simulate what template-context endpoint returns."""
|
||||
resp = CompanyProfileResponse(
|
||||
id="x", tenant_id="t", company_name="Test Corp", legal_form="AG",
|
||||
industry="Finance", founded_year=2015, business_model="B2C",
|
||||
offerings=["Banking"], company_size="large", employee_count="1000+",
|
||||
annual_revenue="> 50 Mio", headquarters_country="DE",
|
||||
headquarters_city="Frankfurt", has_international_locations=True,
|
||||
international_countries=["CH", "AT"], target_markets=["DE", "CH", "AT"],
|
||||
primary_jurisdiction="DE", is_data_controller=True,
|
||||
is_data_processor=True, uses_ai=True, ai_use_cases=["scoring"],
|
||||
dpo_name="Dr. Privacy", dpo_email="dpo@test.de",
|
||||
legal_contact_name="Legal Team", legal_contact_email="legal@test.de",
|
||||
machine_builder=None, is_complete=True,
|
||||
completed_at="2026-01-01", created_at="2025-06-01",
|
||||
updated_at="2026-01-01",
|
||||
ai_systems=[{"name": "Scoring Engine", "risk_category": "high"}],
|
||||
subject_to_ai_act=True, subject_to_nis2=True,
|
||||
review_cycle_months=3,
|
||||
)
|
||||
# Build context dict same as endpoint does
|
||||
ctx = {
|
||||
"company_name": resp.company_name,
|
||||
"dpo_name": resp.dpo_name or "",
|
||||
"uses_ai": resp.uses_ai,
|
||||
"ai_systems": resp.ai_systems,
|
||||
"has_ai_systems": len(resp.ai_systems) > 0,
|
||||
"subject_to_ai_act": resp.subject_to_ai_act,
|
||||
"review_cycle_months": resp.review_cycle_months,
|
||||
}
|
||||
assert ctx["company_name"] == "Test Corp"
|
||||
assert ctx["has_ai_systems"] is True
|
||||
assert ctx["subject_to_ai_act"] is True
|
||||
assert ctx["review_cycle_months"] == 3
|
||||
Reference in New Issue
Block a user