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>
672 lines
23 KiB
Python
672 lines
23 KiB
Python
"""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"}
|