feat(obligations): Go PARTIAL DEPRECATED, Python x-user-id, UCCA Proxy Headers, 62 Tests
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 31s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 19s
CI / test-python-dsms-gateway (push) Successful in 26s

- Go obligations_handlers.go: CRUD-Overlap als deprecated markiert, AI-Features (Assess/Gap/TOM/Export) bleiben aktiv
- Python obligation_routes.py: x-user-id Header + Audit-Logging an 4 Write-Endpoints
- 3 UCCA Proxy-Dateien: Default X-Tenant-ID + X-User-ID Headers
- Tests von 39 auf 62 erweitert (+23 Route-Integration-Tests mit mock_db/TestClient)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-06 21:31:14 +01:00
parent a5e4801b09
commit 3467bce222
6 changed files with 355 additions and 13 deletions

View File

@@ -4,7 +4,11 @@ import pytest
from unittest.mock import MagicMock
from datetime import datetime
from fastapi import FastAPI
from fastapi.testclient import TestClient
from compliance.api.obligation_routes import (
router,
ObligationCreate,
ObligationUpdate,
ObligationStatusUpdate,
@@ -12,6 +16,71 @@ from compliance.api.obligation_routes import (
_get_tenant_id,
DEFAULT_TENANT_ID,
)
from classroom_engine.database import get_db
# ---------------------------------------------------------------------------
# Route-Test infrastructure
# ---------------------------------------------------------------------------
OBLIGATION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
app = FastAPI()
app.include_router(router)
class _MockRow:
"""Simulates a SQLAlchemy row with _mapping attribute."""
def __init__(self, data: dict):
self._mapping = data
def __getitem__(self, idx):
vals = list(self._mapping.values())
return vals[idx]
def _make_obligation_row(overrides=None):
now = datetime(2026, 3, 6, 12, 0, 0)
data = {
"id": OBLIGATION_ID,
"tenant_id": DEFAULT_TENANT_ID,
"title": "Art. 30 VVT führen",
"description": "Pflicht nach DSGVO",
"source": "DSGVO",
"source_article": "Art. 30",
"deadline": None,
"status": "pending",
"priority": "medium",
"responsible": None,
"linked_systems": [],
"assessment_id": None,
"rule_code": None,
"notes": None,
"created_at": now,
"updated_at": now,
}
if overrides:
data.update(overrides)
return _MockRow(data)
def _count_row(val):
"""Simulates a COUNT(*) row — fetchone()[0] returns the value."""
row = MagicMock()
row.__getitem__ = lambda self, idx: val
return row
@pytest.fixture
def mock_db():
db = MagicMock()
app.dependency_overrides[get_db] = lambda: db
yield db
app.dependency_overrides.pop(get_db, None)
@pytest.fixture
def client(mock_db):
return TestClient(app)
# =============================================================================
@@ -323,3 +392,249 @@ class TestObligationBusinessLogic:
for source in ["DSGVO", "AI Act", "NIS2", "BDSG", "ISO 27001"]:
req = ObligationCreate(title="Test", source=source)
assert req.source == source
# =============================================================================
# Route Integration Tests — List
# =============================================================================
class TestListObligationsRoute:
def test_list_empty(self, client, mock_db):
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
MagicMock(fetchall=MagicMock(return_value=[])),
]
resp = client.get("/obligations")
assert resp.status_code == 200
data = resp.json()
assert data["obligations"] == []
assert data["total"] == 0
def test_list_with_items(self, client, mock_db):
row = _make_obligation_row()
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
MagicMock(fetchall=MagicMock(return_value=[row])),
]
resp = client.get("/obligations")
assert resp.status_code == 200
data = resp.json()
assert len(data["obligations"]) == 1
assert data["obligations"][0]["id"] == OBLIGATION_ID
assert data["total"] == 1
def test_list_filter_status(self, client, mock_db):
row = _make_obligation_row({"status": "overdue"})
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
MagicMock(fetchall=MagicMock(return_value=[row])),
]
resp = client.get("/obligations?status=overdue")
assert resp.status_code == 200
assert resp.json()["obligations"][0]["status"] == "overdue"
def test_list_filter_priority(self, client, mock_db):
row = _make_obligation_row({"priority": "critical"})
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
MagicMock(fetchall=MagicMock(return_value=[row])),
]
resp = client.get("/obligations?priority=critical")
assert resp.status_code == 200
assert resp.json()["obligations"][0]["priority"] == "critical"
# =============================================================================
# Route Integration Tests — Create
# =============================================================================
class TestCreateObligationRoute:
def test_create_basic(self, client, mock_db):
row = _make_obligation_row()
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.post("/obligations", json={"title": "Art. 30 VVT führen"})
assert resp.status_code == 201
data = resp.json()
assert data["id"] == OBLIGATION_ID
assert data["title"] == "Art. 30 VVT führen"
mock_db.commit.assert_called_once()
def test_create_full_fields(self, client, mock_db):
row = _make_obligation_row({
"title": "DSFA durchführen",
"source": "DSGVO",
"source_article": "Art. 35",
"priority": "critical",
"responsible": "DSB",
})
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.post("/obligations", json={
"title": "DSFA durchführen",
"source": "DSGVO",
"source_article": "Art. 35",
"priority": "critical",
"responsible": "DSB",
})
assert resp.status_code == 201
assert resp.json()["priority"] == "critical"
assert resp.json()["responsible"] == "DSB"
def test_create_missing_title_422(self, client, mock_db):
resp = client.post("/obligations", json={"source": "DSGVO"})
assert resp.status_code == 422
# =============================================================================
# Route Integration Tests — Get
# =============================================================================
class TestGetObligationRoute:
def test_get_existing(self, client, mock_db):
row = _make_obligation_row()
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.get(f"/obligations/{OBLIGATION_ID}")
assert resp.status_code == 200
assert resp.json()["id"] == OBLIGATION_ID
def test_get_not_found(self, client, mock_db):
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
resp = client.get("/obligations/nonexistent-id")
assert resp.status_code == 404
# =============================================================================
# Route Integration Tests — Update
# =============================================================================
class TestUpdateObligationRoute:
def test_update_partial(self, client, mock_db):
updated = _make_obligation_row({"priority": "high"})
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=updated))
resp = client.put(f"/obligations/{OBLIGATION_ID}", json={"priority": "high"})
assert resp.status_code == 200
assert resp.json()["priority"] == "high"
mock_db.commit.assert_called_once()
def test_update_not_found(self, client, mock_db):
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
resp = client.put(f"/obligations/{OBLIGATION_ID}", json={"title": "Updated"})
assert resp.status_code == 404
def test_update_empty_body_400(self, client, mock_db):
resp = client.put(f"/obligations/{OBLIGATION_ID}", json={})
assert resp.status_code == 400
assert "No fields to update" in resp.json()["detail"]
# =============================================================================
# Route Integration Tests — Status Update
# =============================================================================
class TestUpdateObligationStatusRoute:
def test_valid_status(self, client, mock_db):
row = _make_obligation_row({"status": "in-progress"})
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.put(f"/obligations/{OBLIGATION_ID}/status", json={"status": "in-progress"})
assert resp.status_code == 200
assert resp.json()["status"] == "in-progress"
mock_db.commit.assert_called_once()
def test_invalid_status_400(self, client, mock_db):
resp = client.put(f"/obligations/{OBLIGATION_ID}/status", json={"status": "invalid"})
assert resp.status_code == 400
assert "Invalid status" in resp.json()["detail"]
def test_status_not_found(self, client, mock_db):
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
resp = client.put(f"/obligations/{OBLIGATION_ID}/status", json={"status": "completed"})
assert resp.status_code == 404
def test_status_to_completed(self, client, mock_db):
row = _make_obligation_row({"status": "completed"})
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.put(f"/obligations/{OBLIGATION_ID}/status", json={"status": "completed"})
assert resp.status_code == 200
assert resp.json()["status"] == "completed"
# =============================================================================
# Route Integration Tests — Delete
# =============================================================================
class TestDeleteObligationRoute:
def test_delete_existing(self, client, mock_db):
mock_db.execute.return_value = MagicMock(rowcount=1)
resp = client.delete(f"/obligations/{OBLIGATION_ID}")
assert resp.status_code == 204
mock_db.commit.assert_called_once()
def test_delete_not_found(self, client, mock_db):
mock_db.execute.return_value = MagicMock(rowcount=0)
resp = client.delete(f"/obligations/{OBLIGATION_ID}")
assert resp.status_code == 404
# =============================================================================
# Route Integration Tests — Stats
# =============================================================================
class TestGetObligationStatsRoute:
def test_stats_empty(self, client, mock_db):
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
resp = client.get("/obligations/stats")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 0
assert data["pending"] == 0
def test_stats_with_data(self, client, mock_db):
row = MagicMock()
row._mapping = {
"pending": 3, "in_progress": 2, "overdue": 1,
"completed": 5, "critical": 2, "high": 3, "total": 11,
}
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.get("/obligations/stats")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 11
assert data["pending"] == 3
assert data["critical"] == 2
def test_stats_structure(self, client, mock_db):
row = MagicMock()
row._mapping = {
"pending": 0, "in_progress": 0, "overdue": 0,
"completed": 0, "critical": 0, "high": 0, "total": 0,
}
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
resp = client.get("/obligations/stats")
data = resp.json()
expected_keys = {"pending", "in_progress", "overdue", "completed", "critical", "high", "total"}
assert set(data.keys()) == expected_keys
# =============================================================================
# Route Integration Tests — Search
# =============================================================================
class TestObligationSearchRoute:
def test_search_param(self, client, mock_db):
row = _make_obligation_row({"title": "DSFA Pflicht"})
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
MagicMock(fetchall=MagicMock(return_value=[row])),
]
resp = client.get("/obligations?search=DSFA")
assert resp.status_code == 200
assert len(resp.json()["obligations"]) == 1
def test_source_filter(self, client, mock_db):
row = _make_obligation_row({"source": "AI Act"})
mock_db.execute.side_effect = [
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
MagicMock(fetchall=MagicMock(return_value=[row])),
]
resp = client.get("/obligations?source=AI Act")
assert resp.status_code == 200
assert resp.json()["obligations"][0]["source"] == "AI Act"