feat(compliance-kern): Tests, MkDocs + RAG-Controls Button für Anforderungen/Controls/Nachweise/Risiken
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 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 21s
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 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 21s
- 74 neue Tests (test_risk_routes, test_evidence_routes, test_requirement_routes, test_control_routes) Enum-Mocking (.value), ControlStatusEnum-Validierung, db.query() direkte Mocks - MkDocs: docs-src/services/sdk-modules/compliance-kern.md Endpunkt-Tabellen, Schema-Erklärungen, CI/CD-Beispiele, Risikomatrix - controls/page.tsx: "KI-Controls aus RAG vorschlagen" Button POST /api/sdk/v1/compliance/ai/suggest-controls, Suggestion-Panel, Requirement-ID-Eingabe + Dropdown, Konfidenz-Anzeige, Hinzufügen-Aktion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
345
backend-compliance/tests/test_control_routes.py
Normal file
345
backend-compliance/tests/test_control_routes.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""Tests for Controls routes (routes.py → /compliance/controls).
|
||||
|
||||
Control status enum values: "pass", "partial", "fail", "n/a", "planned"
|
||||
Control domain enum values: "gov", "priv", "iam", "crypto", "sdlc", "ops", "ai", "cra", "aud"
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from compliance.api.routes import router as compliance_router
|
||||
from classroom_engine.database import get_db
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App setup with mocked DB dependency
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(compliance_router)
|
||||
|
||||
mock_db = MagicMock()
|
||||
|
||||
|
||||
def override_get_db():
|
||||
yield mock_db
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
client = TestClient(app)
|
||||
|
||||
CONTROL_UUID = "cccccccc-4444-5555-6666-cccccccccccc"
|
||||
NOW = datetime(2024, 3, 1, 12, 0, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def make_control(overrides=None):
|
||||
c = MagicMock()
|
||||
c.id = CONTROL_UUID
|
||||
c.control_id = "GOV-001"
|
||||
# domain and control_type are Enum → need .value
|
||||
c.domain = MagicMock()
|
||||
c.domain.value = "gov"
|
||||
c.control_type = MagicMock()
|
||||
c.control_type.value = "preventive"
|
||||
c.title = "Datenschutzbeauftragter bestellt"
|
||||
c.description = "DSB nach Art. 37 DSGVO"
|
||||
c.pass_criteria = "DSB bestellt und dokumentiert"
|
||||
c.implementation_guidance = "Ernennung per Urkunde"
|
||||
c.code_reference = None
|
||||
c.documentation_url = None
|
||||
c.is_automated = False
|
||||
c.automation_tool = None
|
||||
c.automation_config = None
|
||||
c.owner = "GF"
|
||||
c.review_frequency_days = 365
|
||||
# status is Enum → need .value
|
||||
c.status = MagicMock()
|
||||
c.status.value = "planned"
|
||||
c.status_notes = None
|
||||
c.last_reviewed_at = None
|
||||
c.next_review_at = None
|
||||
c.created_at = NOW
|
||||
c.updated_at = NOW
|
||||
c.evidence_count = 0
|
||||
c.requirement_count = 1
|
||||
if overrides:
|
||||
for k, v in overrides.items():
|
||||
setattr(c, k, v)
|
||||
return c
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: GET /compliance/controls
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestListControls:
|
||||
"""Tests for GET /compliance/controls."""
|
||||
|
||||
def test_list_empty(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_all.return_value = []
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["controls"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_list_with_control(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_all.return_value = [make_control()]
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
c = data["controls"][0]
|
||||
assert c["control_id"] == "GOV-001"
|
||||
assert c["domain"] == "gov"
|
||||
assert c["is_automated"] is False
|
||||
assert c["status"] == "planned"
|
||||
|
||||
def test_list_filter_domain(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
ctrl = make_control()
|
||||
ctrl.domain.value = "priv"
|
||||
MockRepo.return_value.get_all.return_value = [ctrl]
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls", params={"domain": "priv"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_list_filter_status(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_all.return_value = []
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls", params={"status": "pass"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_list_filter_is_automated(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
ctrl = make_control({"is_automated": True})
|
||||
MockRepo.return_value.get_all.return_value = [ctrl]
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls", params={"is_automated": "true"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_list_filter_search(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_all.return_value = []
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls", params={"search": "Datenschutz"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_list_multiple(self):
|
||||
c2 = make_control()
|
||||
c2.id = "dddddddd-1111-2222-3333-dddddddddddd"
|
||||
c2.control_id = "PRIV-001"
|
||||
c2.domain.value = "priv"
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_all.return_value = [make_control(), c2]
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total"] == 2
|
||||
|
||||
|
||||
class TestListControlsPaginated:
|
||||
"""Tests for GET /compliance/controls/paginated."""
|
||||
|
||||
def test_paginated_empty(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_paginated.return_value = ([], 0)
|
||||
response = client.get("/compliance/controls/paginated")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"] == []
|
||||
assert data["pagination"]["total"] == 0
|
||||
|
||||
def test_paginated_with_data(self):
|
||||
ctrl = make_control()
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_paginated.return_value = ([ctrl], 1)
|
||||
response = client.get("/compliance/controls/paginated", params={"page": 1, "page_size": 25})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["pagination"]["total"] == 1
|
||||
assert data["data"][0]["control_id"] == "GOV-001"
|
||||
|
||||
def test_paginated_filters(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_paginated.return_value = ([], 0)
|
||||
response = client.get(
|
||||
"/compliance/controls/paginated",
|
||||
params={"domain": "gov", "status": "pass"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestGetControlById:
|
||||
"""Tests for GET /compliance/controls/{control_id}."""
|
||||
|
||||
def test_get_existing_by_control_id(self):
|
||||
ctrl = make_control()
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = ctrl
|
||||
MockEvRepo.return_value.get_by_control.return_value = []
|
||||
response = client.get("/compliance/controls/GOV-001")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == CONTROL_UUID
|
||||
assert data["control_id"] == "GOV-001"
|
||||
assert data["domain"] == "gov"
|
||||
assert data["status"] == "planned"
|
||||
|
||||
def test_get_not_found(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = None
|
||||
response = client.get("/compliance/controls/DOES-NOT-EXIST")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_get_evidence_count_included(self):
|
||||
"""Evidence count is included in response."""
|
||||
ctrl = make_control()
|
||||
fake_evidence = [MagicMock(), MagicMock()]
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo, \
|
||||
patch("compliance.api.routes.EvidenceRepository") as MockEvRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = ctrl
|
||||
MockEvRepo.return_value.get_by_control.return_value = fake_evidence
|
||||
response = client.get("/compliance/controls/GOV-001")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["evidence_count"] == 2
|
||||
|
||||
|
||||
class TestUpdateControl:
|
||||
"""Tests for PUT /compliance/controls/{control_id}."""
|
||||
|
||||
def test_update_title(self):
|
||||
updated = make_control()
|
||||
updated.title = "Neuer Titel"
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = make_control()
|
||||
MockRepo.return_value.update.return_value = updated
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001",
|
||||
json={"title": "Neuer Titel"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Neuer Titel"
|
||||
|
||||
def test_update_not_found(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = None
|
||||
response = client.put(
|
||||
"/compliance/controls/DOES-NOT-EXIST",
|
||||
json={"title": "Test"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_status_with_valid_enum(self):
|
||||
"""Status must be a valid ControlStatusEnum value."""
|
||||
updated = make_control()
|
||||
updated.status.value = "pass"
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = make_control()
|
||||
MockRepo.return_value.update.return_value = updated
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001",
|
||||
json={"status": "pass"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_update_status_invalid_enum(self):
|
||||
"""Invalid status → 400."""
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = make_control()
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001",
|
||||
json={"status": "invalid_status"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestReviewControl:
|
||||
"""Tests for PUT /compliance/controls/{control_id}/review."""
|
||||
|
||||
def test_review_success(self):
|
||||
reviewed = make_control()
|
||||
reviewed.status.value = "pass"
|
||||
reviewed.status_notes = "Geprueft OK"
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = make_control()
|
||||
MockRepo.return_value.mark_reviewed.return_value = reviewed
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001/review",
|
||||
json={"status": "pass", "status_notes": "Geprueft OK"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "pass"
|
||||
assert data["status_notes"] == "Geprueft OK"
|
||||
|
||||
def test_review_not_found(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = None
|
||||
response = client.put(
|
||||
"/compliance/controls/DOES-NOT-EXIST/review",
|
||||
json={"status": "pass"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_review_missing_status(self):
|
||||
"""Missing required status field → 422."""
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001/review",
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_review_invalid_status(self):
|
||||
"""Invalid status enum → 400."""
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_control_id.return_value = make_control()
|
||||
response = client.put(
|
||||
"/compliance/controls/GOV-001/review",
|
||||
json={"status": "invalid_status"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestGetControlsByDomain:
|
||||
"""Tests for GET /compliance/controls/by-domain/{domain}."""
|
||||
|
||||
def test_get_by_valid_domain(self):
|
||||
ctrl = make_control()
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_domain.return_value = [ctrl]
|
||||
response = client.get("/compliance/controls/by-domain/gov")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert data["controls"][0]["domain"] == "gov"
|
||||
|
||||
def test_get_by_domain_empty_result(self):
|
||||
with patch("compliance.api.routes.ControlRepository") as MockRepo:
|
||||
MockRepo.return_value.get_by_domain.return_value = []
|
||||
response = client.get("/compliance/controls/by-domain/ai")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total"] == 0
|
||||
|
||||
def test_get_by_invalid_domain(self):
|
||||
"""Invalid domain enum → 400."""
|
||||
response = client.get("/compliance/controls/by-domain/invalid_domain")
|
||||
assert response.status_code == 400
|
||||
Reference in New Issue
Block a user