feat(incidents): Go Incidents nach Python migrieren, Proxy umleiten, 50 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 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
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 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
- incident_routes.py: 15 Endpoints (CRUD, Risk Assessment, Art. 33/34 Notifications, Measures, Timeline, Close, Stats)
- Neuer Endpoint PUT /{id}/status (nicht in Go vorhanden, Frontend braucht ihn)
- Proxy von ai-compliance-sdk:8090 auf backend-compliance:8002 umgeleitet
- Go incidents_handlers.go + main.go als DEPRECATED markiert
- 50/50 Tests bestanden
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
671
backend-compliance/tests/test_incident_routes.py
Normal file
671
backend-compliance/tests/test_incident_routes.py
Normal file
@@ -0,0 +1,671 @@
|
||||
"""Tests for Incident routes (incident_routes.py) — Datenpannen-Management DSGVO Art. 33/34."""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi import FastAPI
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from compliance.api.incident_routes import (
|
||||
router,
|
||||
_calculate_risk_level,
|
||||
_is_notification_required,
|
||||
_calculate_72h_deadline,
|
||||
_incident_to_response,
|
||||
_measure_to_response,
|
||||
_parse_jsonb,
|
||||
DEFAULT_TENANT_ID,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
DEFAULT_TENANT = DEFAULT_TENANT_ID
|
||||
INCIDENT_ID = "ffffffff-0001-0001-0001-000000000001"
|
||||
MEASURE_ID = "ffffffff-0002-0002-0002-000000000002"
|
||||
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helpers — _DictResult / _DictSession pattern for SQLite-free DB mocks
|
||||
# =============================================================================
|
||||
|
||||
class _DictRow:
|
||||
"""Simulates a SQLAlchemy RowMapping (dict-like)."""
|
||||
def __init__(self, data: dict):
|
||||
self._data = data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def keys(self):
|
||||
return self._data.keys()
|
||||
|
||||
def items(self):
|
||||
return self._data.items()
|
||||
|
||||
def values(self):
|
||||
return self._data.values()
|
||||
|
||||
|
||||
class _DictResult:
|
||||
"""Simulates a SQLAlchemy CursorResult."""
|
||||
def __init__(self, rows=None, scalar_val=None):
|
||||
self._rows = rows or []
|
||||
self._scalar_val = scalar_val
|
||||
|
||||
def mappings(self):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return _DictRow(self._rows[0]) if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return [_DictRow(r) for r in self._rows]
|
||||
|
||||
def scalar(self):
|
||||
return self._scalar_val
|
||||
|
||||
|
||||
def make_incident_row(overrides=None):
|
||||
now = datetime(2024, 6, 1, 12, 0, 0)
|
||||
deadline = now + timedelta(hours=72)
|
||||
data = {
|
||||
"id": INCIDENT_ID,
|
||||
"tenant_id": DEFAULT_TENANT,
|
||||
"title": "Datenpanne Test",
|
||||
"description": "Test Beschreibung",
|
||||
"category": "data_breach",
|
||||
"status": "detected",
|
||||
"severity": "medium",
|
||||
"detected_at": now,
|
||||
"reported_by": "admin",
|
||||
"affected_data_categories": json.dumps(["personal"]),
|
||||
"affected_data_subject_count": 100,
|
||||
"affected_systems": json.dumps(["CRM"]),
|
||||
"risk_assessment": None,
|
||||
"authority_notification": json.dumps({
|
||||
"status": "pending",
|
||||
"deadline": deadline.isoformat(),
|
||||
}),
|
||||
"data_subject_notification": json.dumps({
|
||||
"required": False,
|
||||
"status": "not_required",
|
||||
}),
|
||||
"timeline": json.dumps([{
|
||||
"timestamp": now.isoformat(),
|
||||
"action": "incident_created",
|
||||
"user_id": "admin",
|
||||
"details": "Incident detected and reported",
|
||||
}]),
|
||||
"root_cause": None,
|
||||
"lessons_learned": None,
|
||||
"closed_at": None,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
if overrides:
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def make_measure_row(overrides=None):
|
||||
now = datetime(2024, 6, 1, 12, 0, 0)
|
||||
data = {
|
||||
"id": MEASURE_ID,
|
||||
"incident_id": INCIDENT_ID,
|
||||
"title": "Passwort Reset",
|
||||
"description": "Alle betroffenen Passwoerter zuruecksetzen",
|
||||
"measure_type": "corrective",
|
||||
"status": "planned",
|
||||
"responsible": "IT-Admin",
|
||||
"due_date": None,
|
||||
"completed_at": None,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
if overrides:
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
from classroom_engine.database import get_db
|
||||
db = MagicMock()
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
yield db
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper / Utility Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestCalculateRiskLevel:
|
||||
def test_low(self):
|
||||
assert _calculate_risk_level(1, 1) == "low"
|
||||
assert _calculate_risk_level(2, 2) == "low"
|
||||
|
||||
def test_medium(self):
|
||||
assert _calculate_risk_level(2, 3) == "medium"
|
||||
assert _calculate_risk_level(3, 3) == "medium"
|
||||
|
||||
def test_high(self):
|
||||
assert _calculate_risk_level(3, 4) == "high"
|
||||
assert _calculate_risk_level(4, 4) == "high"
|
||||
|
||||
def test_critical(self):
|
||||
assert _calculate_risk_level(4, 5) == "critical"
|
||||
assert _calculate_risk_level(5, 5) == "critical"
|
||||
|
||||
|
||||
class TestIsNotificationRequired:
|
||||
def test_critical_requires(self):
|
||||
assert _is_notification_required("critical") is True
|
||||
|
||||
def test_high_requires(self):
|
||||
assert _is_notification_required("high") is True
|
||||
|
||||
def test_medium_not_required(self):
|
||||
assert _is_notification_required("medium") is False
|
||||
|
||||
def test_low_not_required(self):
|
||||
assert _is_notification_required("low") is False
|
||||
|
||||
|
||||
class TestCalculate72hDeadline:
|
||||
def test_returns_iso_string(self):
|
||||
dt = datetime(2024, 6, 1, 12, 0, 0)
|
||||
result = _calculate_72h_deadline(dt)
|
||||
assert "2024-06-04T12:00:00" in result
|
||||
|
||||
|
||||
class TestParseJsonb:
|
||||
def test_dict_passthrough(self):
|
||||
assert _parse_jsonb({"a": 1}) == {"a": 1}
|
||||
|
||||
def test_list_passthrough(self):
|
||||
assert _parse_jsonb([1, 2]) == [1, 2]
|
||||
|
||||
def test_json_string(self):
|
||||
assert _parse_jsonb('{"a": 1}') == {"a": 1}
|
||||
|
||||
def test_none(self):
|
||||
assert _parse_jsonb(None) is None
|
||||
|
||||
|
||||
class TestIncidentToResponse:
|
||||
def test_parses_jsonb_fields(self):
|
||||
row = _DictRow(make_incident_row())
|
||||
result = _incident_to_response(row)
|
||||
assert isinstance(result["timeline"], list)
|
||||
assert isinstance(result["authority_notification"], dict)
|
||||
|
||||
def test_converts_datetime_to_iso(self):
|
||||
row = _DictRow(make_incident_row())
|
||||
result = _incident_to_response(row)
|
||||
assert "2024-06-01" in result["detected_at"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Create Incident Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestCreateIncident:
|
||||
def test_create_basic(self, mock_db):
|
||||
created_row = make_incident_row()
|
||||
mock_db.execute.return_value = _DictResult([created_row])
|
||||
|
||||
resp = client.post("/incidents", json={
|
||||
"title": "Datenpanne Test",
|
||||
"description": "Test",
|
||||
}, headers={"x-tenant-id": DEFAULT_TENANT, "x-user-id": "admin"})
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "incident" in data
|
||||
assert "authority_deadline" in data
|
||||
assert "hours_until_deadline" in data
|
||||
|
||||
def test_create_with_detected_at(self, mock_db):
|
||||
created_row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
None, # INSERT
|
||||
_DictResult([created_row]), # SELECT back
|
||||
]
|
||||
|
||||
resp = client.post("/incidents", json={
|
||||
"title": "Panne 2",
|
||||
"detected_at": "2024-06-01T10:00:00",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "authority_deadline" in resp.json()
|
||||
|
||||
def test_create_missing_title(self, mock_db):
|
||||
resp = client.post("/incidents", json={})
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# List Incidents Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestListIncidents:
|
||||
def test_list_empty(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult(scalar_val=0),
|
||||
_DictResult([]),
|
||||
]
|
||||
resp = client.get("/incidents", headers={"x-tenant-id": DEFAULT_TENANT})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["incidents"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_list_with_items(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult(scalar_val=1),
|
||||
_DictResult([row]),
|
||||
]
|
||||
resp = client.get("/incidents")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data["incidents"]) == 1
|
||||
assert data["total"] == 1
|
||||
|
||||
def test_list_filter_by_status(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult(scalar_val=0),
|
||||
_DictResult([]),
|
||||
]
|
||||
resp = client.get("/incidents?status=closed")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Get Incident Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestGetIncident:
|
||||
def test_get_existing(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]), # incident
|
||||
_DictResult([]), # measures
|
||||
]
|
||||
resp = client.get(f"/incidents/{INCIDENT_ID}")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "incident" in data
|
||||
assert "measures" in data
|
||||
assert "deadline_info" in data
|
||||
|
||||
def test_get_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.get(f"/incidents/{UNKNOWN_ID}")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Update Incident Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestUpdateIncident:
|
||||
def test_update_title(self, mock_db):
|
||||
row = make_incident_row({"title": "Updated Title"})
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]), # check exists
|
||||
None, # update
|
||||
_DictResult([row]), # fetch back
|
||||
]
|
||||
resp = client.put(f"/incidents/{INCIDENT_ID}", json={"title": "Updated Title"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["incident"]["title"] == "Updated Title"
|
||||
|
||||
def test_update_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.put(f"/incidents/{UNKNOWN_ID}", json={"title": "X"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Delete Incident Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestDeleteIncident:
|
||||
def test_delete_existing(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]), # check exists
|
||||
None, # delete measures
|
||||
None, # delete incident
|
||||
]
|
||||
resp = client.delete(f"/incidents/{INCIDENT_ID}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "incident deleted"
|
||||
|
||||
def test_delete_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.delete(f"/incidents/{UNKNOWN_ID}")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Status Update Tests (NEW endpoint)
|
||||
# =============================================================================
|
||||
|
||||
class TestStatusUpdate:
|
||||
def test_update_status(self, mock_db):
|
||||
row = make_incident_row({"status": "assessment"})
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]), # check exists
|
||||
None, # update status
|
||||
None, # append timeline
|
||||
_DictResult([row]), # fetch back
|
||||
]
|
||||
resp = client.put(f"/incidents/{INCIDENT_ID}/status", json={"status": "assessment"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["incident"]["status"] == "assessment"
|
||||
|
||||
def test_status_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.put(f"/incidents/{UNKNOWN_ID}/status", json={"status": "closed"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Risk Assessment Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestAssessRisk:
|
||||
def test_low_risk(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]), # check exists
|
||||
None, # update risk_assessment + status
|
||||
None, # append timeline
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
|
||||
"likelihood": 1,
|
||||
"impact": 2,
|
||||
"notes": "Low risk",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["risk_assessment"]["risk_level"] == "low"
|
||||
assert data["notification_required"] is False
|
||||
|
||||
def test_high_risk_requires_notification(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]), # check exists
|
||||
None, # update authority_notification
|
||||
None, # update risk_assessment + status
|
||||
None, # append timeline
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
|
||||
"likelihood": 4,
|
||||
"impact": 4,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["risk_assessment"]["risk_level"] == "high"
|
||||
assert data["notification_required"] is True
|
||||
assert data["incident_status"] == "notification_required"
|
||||
|
||||
def test_critical_risk(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]),
|
||||
None, None, None,
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/assess-risk", json={
|
||||
"likelihood": 5,
|
||||
"impact": 5,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["risk_assessment"]["risk_level"] == "critical"
|
||||
|
||||
def test_assess_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.post(f"/incidents/{UNKNOWN_ID}/assess-risk", json={
|
||||
"likelihood": 3, "impact": 3,
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Authority Notification Tests (Art. 33)
|
||||
# =============================================================================
|
||||
|
||||
class TestNotifyAuthority:
|
||||
def test_notify_authority(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]), # check exists
|
||||
None, # update authority_notification + status
|
||||
None, # append timeline
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/notify-authority", json={
|
||||
"authority_name": "LfD Bayern",
|
||||
"reference_number": "REF-2024-001",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["authority_notification"]["authority_name"] == "LfD Bayern"
|
||||
assert "submitted_within_72h" in data
|
||||
|
||||
def test_notify_authority_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.post(f"/incidents/{UNKNOWN_ID}/notify-authority", json={
|
||||
"authority_name": "Test",
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Subject Notification Tests (Art. 34)
|
||||
# =============================================================================
|
||||
|
||||
class TestNotifySubjects:
|
||||
def test_notify_subjects(self, mock_db):
|
||||
row = make_incident_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([row]),
|
||||
None, None,
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/notify-subjects", json={
|
||||
"notification_text": "Ihre Daten waren betroffen",
|
||||
"channel": "email",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["data_subject_notification"]["status"] == "sent"
|
||||
assert data["data_subject_notification"]["channel"] == "email"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Measures Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestMeasures:
|
||||
def test_add_measure(self, mock_db):
|
||||
measure_row = make_measure_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]), # check exists
|
||||
None, # insert measure
|
||||
None, # append timeline
|
||||
_DictResult([measure_row]), # fetch measure
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/measures", json={
|
||||
"title": "Passwort Reset",
|
||||
"measure_type": "corrective",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["measure"]["title"] == "Passwort Reset"
|
||||
|
||||
def test_add_measure_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.post(f"/incidents/{UNKNOWN_ID}/measures", json={
|
||||
"title": "Test",
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_update_measure(self, mock_db):
|
||||
updated_row = make_measure_row({"title": "Neuer Titel"})
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": MEASURE_ID}]), # check exists
|
||||
None, # update
|
||||
_DictResult([updated_row]), # fetch back
|
||||
]
|
||||
resp = client.put(
|
||||
f"/incidents/{INCIDENT_ID}/measures/{MEASURE_ID}",
|
||||
json={"title": "Neuer Titel"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["measure"]["title"] == "Neuer Titel"
|
||||
|
||||
def test_complete_measure(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": MEASURE_ID}]),
|
||||
None,
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/measures/{MEASURE_ID}/complete")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "measure completed"
|
||||
|
||||
def test_measures_in_get_response(self, mock_db):
|
||||
inc_row = make_incident_row()
|
||||
meas_row = make_measure_row()
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([inc_row]),
|
||||
_DictResult([meas_row]),
|
||||
]
|
||||
resp = client.get(f"/incidents/{INCIDENT_ID}")
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()["measures"]) == 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Timeline Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestTimeline:
|
||||
def test_add_timeline_entry(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]),
|
||||
None,
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/timeline", json={
|
||||
"action": "investigation_started",
|
||||
"details": "Forensic analysis begun",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["timeline_entry"]["action"] == "investigation_started"
|
||||
|
||||
def test_timeline_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.post(f"/incidents/{UNKNOWN_ID}/timeline", json={
|
||||
"action": "test",
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_auto_timeline_on_create(self, mock_db):
|
||||
"""Create endpoint auto-adds incident_created timeline entry."""
|
||||
row = make_incident_row()
|
||||
mock_db.execute.return_value = _DictResult([row])
|
||||
|
||||
resp = client.post("/incidents", json={"title": "Test"})
|
||||
assert resp.status_code == 200
|
||||
# The insert SQL includes a timeline with incident_created entry
|
||||
insert_call = mock_db.execute.call_args_list[0]
|
||||
sql_text = str(insert_call[0][0])
|
||||
assert "incident_incidents" in sql_text
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Close Incident Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestCloseIncident:
|
||||
def test_close_with_root_cause(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]),
|
||||
None, # update
|
||||
None, # timeline
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/close", json={
|
||||
"root_cause": "Schwache Passwoerter",
|
||||
"lessons_learned": "2FA einfuehren",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["message"] == "incident closed"
|
||||
assert data["root_cause"] == "Schwache Passwoerter"
|
||||
|
||||
def test_close_minimal(self, mock_db):
|
||||
mock_db.execute.side_effect = [
|
||||
_DictResult([{"id": INCIDENT_ID}]),
|
||||
None, None,
|
||||
]
|
||||
resp = client.post(f"/incidents/{INCIDENT_ID}/close", json={
|
||||
"root_cause": "Unbekannt",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_close_not_found(self, mock_db):
|
||||
mock_db.execute.return_value = _DictResult([])
|
||||
resp = client.post(f"/incidents/{UNKNOWN_ID}/close", json={
|
||||
"root_cause": "Test",
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Statistics Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestStatistics:
|
||||
def test_empty_stats(self, mock_db):
|
||||
stats_row = {
|
||||
"total": 0, "open": 0, "closed": 0,
|
||||
"critical": 0, "high": 0, "medium": 0, "low": 0,
|
||||
}
|
||||
mock_db.execute.return_value = _DictResult([stats_row])
|
||||
resp = client.get("/incidents/stats")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 0
|
||||
assert data["by_severity"]["critical"] == 0
|
||||
|
||||
def test_stats_with_data(self, mock_db):
|
||||
stats_row = {
|
||||
"total": 5, "open": 3, "closed": 2,
|
||||
"critical": 1, "high": 2, "medium": 1, "low": 1,
|
||||
}
|
||||
mock_db.execute.return_value = _DictResult([stats_row])
|
||||
resp = client.get("/incidents/stats")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 5
|
||||
assert data["open"] == 3
|
||||
assert data["closed"] == 2
|
||||
assert data["by_severity"]["critical"] == 1
|
||||
|
||||
def test_stats_structure(self, mock_db):
|
||||
stats_row = {
|
||||
"total": 1, "open": 1, "closed": 0,
|
||||
"critical": 0, "high": 0, "medium": 1, "low": 0,
|
||||
}
|
||||
mock_db.execute.return_value = _DictResult([stats_row])
|
||||
resp = client.get("/incidents/stats")
|
||||
data = resp.json()
|
||||
assert set(data.keys()) == {"total", "open", "closed", "by_severity"}
|
||||
assert set(data["by_severity"].keys()) == {"critical", "high", "medium", "low"}
|
||||
Reference in New Issue
Block a user