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>
235 lines
8.3 KiB
Python
235 lines
8.3 KiB
Python
"""Tests for Document Versioning (Phase 3).
|
|
|
|
Verifies:
|
|
- versioning_utils: create_version_snapshot, list_versions, get_version
|
|
- VERSION_TABLES mapping is correct
|
|
- Version endpoints are registered on all 5 route files
|
|
"""
|
|
|
|
import pytest
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
from datetime import datetime
|
|
|
|
from compliance.api.versioning_utils import (
|
|
VERSION_TABLES,
|
|
create_version_snapshot,
|
|
list_versions,
|
|
get_version,
|
|
)
|
|
|
|
TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
|
|
DOC_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
|
|
|
|
# =============================================================================
|
|
# VERSION_TABLES mapping
|
|
# =============================================================================
|
|
|
|
class TestVersionTablesMapping:
|
|
def test_all_5_doc_types(self):
|
|
assert "dsfa" in VERSION_TABLES
|
|
assert "vvt_activity" in VERSION_TABLES
|
|
assert "tom" in VERSION_TABLES
|
|
assert "loeschfristen" in VERSION_TABLES
|
|
assert "obligation" in VERSION_TABLES
|
|
assert len(VERSION_TABLES) == 5
|
|
|
|
def test_dsfa_mapping(self):
|
|
table, fk, source = VERSION_TABLES["dsfa"]
|
|
assert table == "compliance_dsfa_versions"
|
|
assert fk == "dsfa_id"
|
|
assert source == "compliance_dsfas"
|
|
|
|
def test_vvt_mapping(self):
|
|
table, fk, source = VERSION_TABLES["vvt_activity"]
|
|
assert table == "compliance_vvt_activity_versions"
|
|
assert fk == "activity_id"
|
|
assert source == "compliance_vvt_activities"
|
|
|
|
def test_tom_mapping(self):
|
|
table, fk, source = VERSION_TABLES["tom"]
|
|
assert table == "compliance_tom_versions"
|
|
assert fk == "measure_id"
|
|
assert source == "compliance_tom_measures"
|
|
|
|
def test_loeschfristen_mapping(self):
|
|
table, fk, source = VERSION_TABLES["loeschfristen"]
|
|
assert table == "compliance_loeschfristen_versions"
|
|
assert fk == "policy_id"
|
|
assert source == "compliance_loeschfristen"
|
|
|
|
def test_obligation_mapping(self):
|
|
table, fk, source = VERSION_TABLES["obligation"]
|
|
assert table == "compliance_obligation_versions"
|
|
assert fk == "obligation_id"
|
|
assert source == "compliance_obligations"
|
|
|
|
|
|
# =============================================================================
|
|
# create_version_snapshot
|
|
# =============================================================================
|
|
|
|
class TestCreateVersionSnapshot:
|
|
def test_invalid_doc_type_raises(self):
|
|
db = MagicMock()
|
|
with pytest.raises(ValueError, match="Unknown document type"):
|
|
create_version_snapshot(db, "invalid_type", DOC_ID, TENANT, {"data": 1})
|
|
|
|
def test_creates_version_with_correct_params(self):
|
|
db = MagicMock()
|
|
|
|
# Mock: MAX(version_number) returns 0 (first version)
|
|
max_result = MagicMock()
|
|
max_result.scalar.return_value = 0
|
|
|
|
# Mock: INSERT RETURNING
|
|
insert_result = MagicMock()
|
|
insert_result.fetchone.return_value = (
|
|
"new-uuid",
|
|
1,
|
|
datetime(2026, 3, 7, 12, 0, 0),
|
|
)
|
|
|
|
# Mock: UPDATE (returns nothing)
|
|
update_result = MagicMock()
|
|
|
|
db.execute.side_effect = [max_result, insert_result, update_result]
|
|
|
|
result = create_version_snapshot(
|
|
db, "dsfa", DOC_ID, TENANT,
|
|
snapshot={"title": "Test DSFA"},
|
|
change_summary="Initial version",
|
|
created_by="test-user",
|
|
)
|
|
|
|
assert result["version_number"] == 1
|
|
assert result["id"] == "new-uuid"
|
|
assert db.execute.call_count == 3
|
|
|
|
def test_increments_version_number(self):
|
|
db = MagicMock()
|
|
|
|
# MAX returns 2 (existing 2 versions)
|
|
max_result = MagicMock()
|
|
max_result.scalar.return_value = 2
|
|
|
|
insert_result = MagicMock()
|
|
insert_result.fetchone.return_value = ("uuid-3", 3, datetime(2026, 3, 7))
|
|
|
|
update_result = MagicMock()
|
|
db.execute.side_effect = [max_result, insert_result, update_result]
|
|
|
|
result = create_version_snapshot(
|
|
db, "vvt_activity", DOC_ID, TENANT,
|
|
snapshot={"name": "Activity"},
|
|
)
|
|
assert result["version_number"] == 3
|
|
|
|
|
|
# =============================================================================
|
|
# list_versions
|
|
# =============================================================================
|
|
|
|
class TestListVersions:
|
|
def test_invalid_doc_type_raises(self):
|
|
db = MagicMock()
|
|
with pytest.raises(ValueError, match="Unknown document type"):
|
|
list_versions(db, "invalid", DOC_ID, TENANT)
|
|
|
|
def test_returns_formatted_list(self):
|
|
db = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.fetchall.return_value = [
|
|
("uuid-1", 1, "draft", "Initial", [], "system", None, None, datetime(2026, 3, 1)),
|
|
("uuid-2", 2, "approved", "Updated measures", ["section3"], "admin", "dpo", datetime(2026, 3, 5), datetime(2026, 3, 2)),
|
|
]
|
|
db.execute.return_value = mock_result
|
|
|
|
result = list_versions(db, "tom", DOC_ID, TENANT)
|
|
assert len(result) == 2
|
|
assert result[0]["version_number"] == 1
|
|
assert result[0]["status"] == "draft"
|
|
assert result[1]["version_number"] == 2
|
|
assert result[1]["approved_by"] == "dpo"
|
|
|
|
def test_empty_list(self):
|
|
db = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.fetchall.return_value = []
|
|
db.execute.return_value = mock_result
|
|
|
|
result = list_versions(db, "obligation", DOC_ID, TENANT)
|
|
assert result == []
|
|
|
|
|
|
# =============================================================================
|
|
# get_version
|
|
# =============================================================================
|
|
|
|
class TestGetVersion:
|
|
def test_invalid_doc_type_raises(self):
|
|
db = MagicMock()
|
|
with pytest.raises(ValueError, match="Unknown document type"):
|
|
get_version(db, "invalid", DOC_ID, 1, TENANT)
|
|
|
|
def test_returns_version_with_snapshot(self):
|
|
db = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.fetchone.return_value = (
|
|
"uuid-1", 1, "draft", {"title": "Test", "status": "draft"},
|
|
"Initial version", ["section1"], "system", None, None, datetime(2026, 3, 1),
|
|
)
|
|
db.execute.return_value = mock_result
|
|
|
|
result = get_version(db, "loeschfristen", DOC_ID, 1, TENANT)
|
|
assert result is not None
|
|
assert result["version_number"] == 1
|
|
assert result["snapshot"]["title"] == "Test"
|
|
assert result["change_summary"] == "Initial version"
|
|
|
|
def test_returns_none_for_missing(self):
|
|
db = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.fetchone.return_value = None
|
|
db.execute.return_value = mock_result
|
|
|
|
result = get_version(db, "dsfa", DOC_ID, 99, TENANT)
|
|
assert result is None
|
|
|
|
|
|
# =============================================================================
|
|
# Route Registration Tests
|
|
# =============================================================================
|
|
|
|
class TestVersionEndpointsRegistered:
|
|
"""Verify all 5 route files have version endpoints."""
|
|
|
|
def _has_route(self, router, suffix):
|
|
return any(r.path.endswith(suffix) for r in router.routes)
|
|
|
|
def test_dsfa_has_version_routes(self):
|
|
from compliance.api.dsfa_routes import router
|
|
assert self._has_route(router, "/versions")
|
|
assert self._has_route(router, "/versions/{version_number}")
|
|
|
|
def test_vvt_has_version_routes(self):
|
|
from compliance.api.vvt_routes import router
|
|
assert self._has_route(router, "/versions")
|
|
assert self._has_route(router, "/versions/{version_number}")
|
|
|
|
def test_tom_has_version_routes(self):
|
|
from compliance.api.tom_routes import router
|
|
assert self._has_route(router, "/versions")
|
|
assert self._has_route(router, "/versions/{version_number}")
|
|
|
|
def test_loeschfristen_has_version_routes(self):
|
|
from compliance.api.loeschfristen_routes import router
|
|
assert self._has_route(router, "/versions")
|
|
assert self._has_route(router, "/versions/{version_number}")
|
|
|
|
def test_obligation_has_version_routes(self):
|
|
from compliance.api.obligation_routes import router
|
|
assert self._has_route(router, "/versions")
|
|
assert self._has_route(router, "/versions/{version_number}")
|