Files
breakpilot-compliance/backend-compliance/tests/test_document_versions.py
Benjamin Admin 1e84df9769
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
feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6)
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>
2026-03-07 14:12:34 +01:00

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}")