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
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user