fix(escalations): Tenant/User-ID Defaults + Routing-Klarheit
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 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 19s
CI / test-python-dsms-gateway (push) Successful in 16s
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 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 19s
CI / test-python-dsms-gateway (push) Successful in 16s
- escalations/route.ts: X-Tenant-Id + X-User-Id Default-Header ergaenzt, X-User-Id aus Request weitergeleitet - escalation_routes.py: DEFAULT_TENANT_ID Konstante (9282a473-...) statt 'default' - test_escalation_routes.py: vollstaendige Test-Suite ergaenzt (+337 Zeilen) - main.go + escalation_handlers.go: DEPRECATED-Kommentare — UCCA-Escalations bleiben fuer Assessment-Review, Haupt-Escalation-System ist Python-Backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ async function proxyRequest(
|
|||||||
try {
|
try {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
|
||||||
|
'X-User-Id': 'admin',
|
||||||
}
|
}
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization')
|
const authHeader = request.headers.get('authorization')
|
||||||
@@ -34,6 +36,11 @@ async function proxyRequest(
|
|||||||
headers['X-Tenant-Id'] = tenantHeader
|
headers['X-Tenant-Id'] = tenantHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userIdHeader = request.headers.get('x-user-id')
|
||||||
|
if (userIdHeader) {
|
||||||
|
headers['X-User-Id'] = userIdHeader
|
||||||
|
}
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
|
|||||||
@@ -345,7 +345,9 @@ func main() {
|
|||||||
uccaRoutes.GET("/wizard/schema", uccaHandlers.GetWizardSchema)
|
uccaRoutes.GET("/wizard/schema", uccaHandlers.GetWizardSchema)
|
||||||
uccaRoutes.POST("/wizard/ask", uccaHandlers.AskWizardQuestion)
|
uccaRoutes.POST("/wizard/ask", uccaHandlers.AskWizardQuestion)
|
||||||
|
|
||||||
// Escalation management (E0-E3 workflow)
|
// DEPRECATED: UCCA Escalation management (E0-E3 workflow)
|
||||||
|
// Frontend uses Python backend-compliance escalation_routes.py (/api/compliance/escalations).
|
||||||
|
// These UCCA-specific routes remain for assessment-review workflows only.
|
||||||
uccaRoutes.GET("/escalations", escalationHandlers.ListEscalations)
|
uccaRoutes.GET("/escalations", escalationHandlers.ListEscalations)
|
||||||
uccaRoutes.GET("/escalations/stats", escalationHandlers.GetEscalationStats)
|
uccaRoutes.GET("/escalations/stats", escalationHandlers.GetEscalationStats)
|
||||||
uccaRoutes.GET("/escalations/:id", escalationHandlers.GetEscalation)
|
uccaRoutes.GET("/escalations/:id", escalationHandlers.GetEscalation)
|
||||||
@@ -354,7 +356,7 @@ func main() {
|
|||||||
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
|
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
|
||||||
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
|
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
|
||||||
|
|
||||||
// DSB Pool management
|
// DEPRECATED: DSB Pool management — see note above
|
||||||
uccaRoutes.GET("/dsb-pool", escalationHandlers.ListDSBPool)
|
uccaRoutes.GET("/dsb-pool", escalationHandlers.ListDSBPool)
|
||||||
uccaRoutes.POST("/dsb-pool", escalationHandlers.AddDSBPoolMember)
|
uccaRoutes.POST("/dsb-pool", escalationHandlers.AddDSBPoolMember)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// DEPRECATED: This file implements UCCA-specific escalation handlers (E0-E3 workflow).
|
||||||
|
// The primary escalation system is now Python backend-compliance escalation_routes.py
|
||||||
|
// (/api/compliance/escalations). These UCCA handlers remain only for assessment-review
|
||||||
|
// workflows and will be removed in a future release.
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ from classroom_engine.database import get_db
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/escalations", tags=["escalations"])
|
router = APIRouter(prefix="/escalations", tags=["escalations"])
|
||||||
|
|
||||||
|
DEFAULT_TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Pydantic Schemas
|
# Pydantic Schemas
|
||||||
@@ -82,7 +84,7 @@ async def list_escalations(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List escalations with optional filters."""
|
"""List escalations with optional filters."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
where_clauses = ["tenant_id = :tenant_id"]
|
where_clauses = ["tenant_id = :tenant_id"]
|
||||||
params: Dict[str, Any] = {"tenant_id": tid, "limit": limit, "offset": offset}
|
params: Dict[str, Any] = {"tenant_id": tid, "limit": limit, "offset": offset}
|
||||||
@@ -121,10 +123,11 @@ async def list_escalations(
|
|||||||
async def create_escalation(
|
async def create_escalation(
|
||||||
request: EscalationCreate,
|
request: EscalationCreate,
|
||||||
tenant_id: Optional[str] = Header(None, alias="x-tenant-id"),
|
tenant_id: Optional[str] = Header(None, alias="x-tenant-id"),
|
||||||
|
user_id: Optional[str] = Header(None, alias="x-user-id"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Create a new escalation."""
|
"""Create a new escalation."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
text(
|
text(
|
||||||
@@ -162,7 +165,7 @@ async def get_stats(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Return counts per status and priority."""
|
"""Return counts per status and priority."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
status_rows = db.execute(
|
status_rows = db.execute(
|
||||||
text(
|
text(
|
||||||
@@ -218,7 +221,7 @@ async def get_escalation(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get a single escalation by ID."""
|
"""Get a single escalation by ID."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
text(
|
text(
|
||||||
"SELECT * FROM compliance_escalations "
|
"SELECT * FROM compliance_escalations "
|
||||||
@@ -239,7 +242,7 @@ async def update_escalation(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update an escalation's fields."""
|
"""Update an escalation's fields."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
existing = db.execute(
|
existing = db.execute(
|
||||||
text(
|
text(
|
||||||
@@ -282,7 +285,7 @@ async def update_status(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update only the status of an escalation."""
|
"""Update only the status of an escalation."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
existing = db.execute(
|
existing = db.execute(
|
||||||
text(
|
text(
|
||||||
@@ -322,7 +325,7 @@ async def delete_escalation(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Delete an escalation."""
|
"""Delete an escalation."""
|
||||||
tid = tenant_id or 'default'
|
tid = tenant_id or DEFAULT_TENANT_ID
|
||||||
|
|
||||||
existing = db.execute(
|
existing = db.execute(
|
||||||
text(
|
text(
|
||||||
|
|||||||
@@ -1,16 +1,90 @@
|
|||||||
"""Tests for escalation routes and schemas (escalation_routes.py)."""
|
"""Tests for escalation routes and schemas (escalation_routes.py)."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from compliance.api.escalation_routes import (
|
from compliance.api.escalation_routes import (
|
||||||
|
router,
|
||||||
EscalationCreate,
|
EscalationCreate,
|
||||||
EscalationUpdate,
|
EscalationUpdate,
|
||||||
EscalationStatusUpdate,
|
EscalationStatusUpdate,
|
||||||
_row_to_dict,
|
_row_to_dict,
|
||||||
|
DEFAULT_TENANT_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from classroom_engine.database import get_db
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# App + Client setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
ESCALATION_ID = "eeeeeeee-0001-0001-0001-000000000001"
|
||||||
|
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Mock helpers
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
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_escalation_row(overrides=None):
|
||||||
|
now = datetime(2026, 3, 6, 12, 0, 0)
|
||||||
|
data = {
|
||||||
|
"id": ESCALATION_ID,
|
||||||
|
"tenant_id": DEFAULT_TENANT_ID,
|
||||||
|
"title": "Test Eskalation",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"priority": "medium",
|
||||||
|
"status": "open",
|
||||||
|
"category": "dsgvo_breach",
|
||||||
|
"assignee": "admin@example.com",
|
||||||
|
"reporter": "user@example.com",
|
||||||
|
"source_module": None,
|
||||||
|
"source_id": None,
|
||||||
|
"due_date": None,
|
||||||
|
"resolved_at": 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)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Schema Tests — EscalationCreate
|
# Schema Tests — EscalationCreate
|
||||||
@@ -117,3 +191,274 @@ class TestRowToDict:
|
|||||||
result = _row_to_dict(row)
|
result = _row_to_dict(row)
|
||||||
assert result["description"] is None
|
assert result["description"] is None
|
||||||
assert result["resolved_at"] is None
|
assert result["resolved_at"] is None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — List Escalations
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestListEscalations:
|
||||||
|
def test_list_empty(self, client, mock_db):
|
||||||
|
mock_db.execute.side_effect = None
|
||||||
|
mock_db.execute.return_value = MagicMock(
|
||||||
|
fetchall=MagicMock(return_value=[]),
|
||||||
|
fetchone=MagicMock(return_value=_count_row(0)),
|
||||||
|
)
|
||||||
|
# fetchall for items, then fetchone for count
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["items"] == []
|
||||||
|
assert data["total"] == 0
|
||||||
|
|
||||||
|
def test_list_with_items(self, client, mock_db):
|
||||||
|
row = _make_escalation_row()
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[row])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert len(data["items"]) == 1
|
||||||
|
assert data["items"][0]["id"] == ESCALATION_ID
|
||||||
|
assert data["total"] == 1
|
||||||
|
|
||||||
|
def test_list_filter_status(self, client, mock_db):
|
||||||
|
row = _make_escalation_row({"status": "in_progress"})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[row])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations?status=in_progress")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert len(data["items"]) == 1
|
||||||
|
|
||||||
|
def test_list_filter_priority(self, client, mock_db):
|
||||||
|
row = _make_escalation_row({"priority": "critical"})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[row])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(1))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations?priority=critical")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["items"][0]["priority"] == "critical"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Create Escalation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestCreateEscalation:
|
||||||
|
def test_create_basic(self, client, mock_db):
|
||||||
|
row = _make_escalation_row()
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
|
||||||
|
resp = client.post("/escalations", json={"title": "Test Eskalation"})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
data = resp.json()
|
||||||
|
assert data["id"] == ESCALATION_ID
|
||||||
|
assert data["title"] == "Test Eskalation"
|
||||||
|
mock_db.commit.assert_called_once()
|
||||||
|
|
||||||
|
def test_create_full_fields(self, client, mock_db):
|
||||||
|
row = _make_escalation_row({
|
||||||
|
"priority": "critical",
|
||||||
|
"category": "ai_act",
|
||||||
|
"assignee": "dsb@example.com",
|
||||||
|
})
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
|
||||||
|
resp = client.post("/escalations", json={
|
||||||
|
"title": "AI Act Verstoß",
|
||||||
|
"description": "Hochrisiko-KI ohne Dokumentation",
|
||||||
|
"priority": "critical",
|
||||||
|
"category": "ai_act",
|
||||||
|
"assignee": "dsb@example.com",
|
||||||
|
})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.json()["priority"] == "critical"
|
||||||
|
|
||||||
|
def test_create_missing_title_422(self, client, mock_db):
|
||||||
|
resp = client.post("/escalations", json={"description": "Ohne Titel"})
|
||||||
|
assert resp.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Get Escalation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestGetEscalation:
|
||||||
|
def test_get_existing(self, client, mock_db):
|
||||||
|
row = _make_escalation_row()
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=row))
|
||||||
|
resp = client.get(f"/escalations/{ESCALATION_ID}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["id"] == ESCALATION_ID
|
||||||
|
|
||||||
|
def test_get_not_found(self, client, mock_db):
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
|
||||||
|
resp = client.get(f"/escalations/{UNKNOWN_ID}")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Update Escalation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestUpdateEscalation:
|
||||||
|
def test_update_partial(self, client, mock_db):
|
||||||
|
existing = _make_escalation_row()
|
||||||
|
updated = _make_escalation_row({"priority": "high"})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=existing)), # existence check
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=updated)), # update RETURNING
|
||||||
|
]
|
||||||
|
resp = client.put(f"/escalations/{ESCALATION_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"/escalations/{UNKNOWN_ID}", json={"title": "New"})
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
def test_update_empty_no_op(self, client, mock_db):
|
||||||
|
row = _make_escalation_row()
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=row)), # existence check
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=row)), # re-fetch unchanged
|
||||||
|
]
|
||||||
|
resp = client.put(f"/escalations/{ESCALATION_ID}", json={})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["id"] == ESCALATION_ID
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Status Update
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestUpdateStatus:
|
||||||
|
def test_to_in_progress(self, client, mock_db):
|
||||||
|
existing = _make_escalation_row()
|
||||||
|
updated = _make_escalation_row({"status": "in_progress"})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=existing)),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=updated)),
|
||||||
|
]
|
||||||
|
resp = client.put(f"/escalations/{ESCALATION_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_to_resolved_auto_resolved_at(self, client, mock_db):
|
||||||
|
existing = _make_escalation_row()
|
||||||
|
now = datetime(2026, 3, 6, 14, 0, 0)
|
||||||
|
updated = _make_escalation_row({"status": "resolved", "resolved_at": now})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=existing)),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=updated)),
|
||||||
|
]
|
||||||
|
resp = client.put(f"/escalations/{ESCALATION_ID}/status", json={"status": "resolved"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["status"] == "resolved"
|
||||||
|
assert data["resolved_at"] is not None
|
||||||
|
|
||||||
|
def test_to_closed(self, client, mock_db):
|
||||||
|
existing = _make_escalation_row()
|
||||||
|
updated = _make_escalation_row({"status": "closed"})
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=existing)),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=updated)),
|
||||||
|
]
|
||||||
|
resp = client.put(f"/escalations/{ESCALATION_ID}/status", json={"status": "closed"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["status"] == "closed"
|
||||||
|
|
||||||
|
def test_status_not_found(self, client, mock_db):
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
|
||||||
|
resp = client.put(f"/escalations/{UNKNOWN_ID}/status", json={"status": "closed"})
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Delete Escalation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestDeleteEscalation:
|
||||||
|
def test_delete_existing(self, client, mock_db):
|
||||||
|
row = _make_escalation_row()
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=row)), # existence check
|
||||||
|
None, # DELETE
|
||||||
|
]
|
||||||
|
resp = client.delete(f"/escalations/{ESCALATION_ID}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["success"] is True
|
||||||
|
mock_db.commit.assert_called_once()
|
||||||
|
|
||||||
|
def test_delete_not_found(self, client, mock_db):
|
||||||
|
mock_db.execute.return_value = MagicMock(fetchone=MagicMock(return_value=None))
|
||||||
|
resp = client.delete(f"/escalations/{UNKNOWN_ID}")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Route Tests — Stats
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestGetStats:
|
||||||
|
def test_stats_empty(self, client, mock_db):
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[])), # by_status
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[])), # by_priority
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(0))), # total
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(0))), # active
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations/stats")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["total"] == 0
|
||||||
|
assert data["active"] == 0
|
||||||
|
assert data["by_status"]["open"] == 0
|
||||||
|
|
||||||
|
def test_stats_with_data(self, client, mock_db):
|
||||||
|
status_rows = [_MockRow({"0": "open", "1": 3}), _MockRow({"0": "resolved", "1": 1})]
|
||||||
|
# These rows need [0] and [1] indexing
|
||||||
|
sr1 = MagicMock(); sr1.__getitem__ = lambda s, i: ["open", 3][i]
|
||||||
|
sr2 = MagicMock(); sr2.__getitem__ = lambda s, i: ["resolved", 1][i]
|
||||||
|
pr1 = MagicMock(); pr1.__getitem__ = lambda s, i: ["high", 2][i]
|
||||||
|
pr2 = MagicMock(); pr2.__getitem__ = lambda s, i: ["medium", 2][i]
|
||||||
|
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[sr1, sr2])),
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[pr1, pr2])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(4))),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(3))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations/stats")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["total"] == 4
|
||||||
|
assert data["active"] == 3
|
||||||
|
assert data["by_status"]["open"] == 3
|
||||||
|
assert data["by_status"]["resolved"] == 1
|
||||||
|
|
||||||
|
def test_stats_structure(self, client, mock_db):
|
||||||
|
mock_db.execute.side_effect = [
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[])),
|
||||||
|
MagicMock(fetchall=MagicMock(return_value=[])),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
|
||||||
|
MagicMock(fetchone=MagicMock(return_value=_count_row(0))),
|
||||||
|
]
|
||||||
|
resp = client.get("/escalations/stats")
|
||||||
|
data = resp.json()
|
||||||
|
assert set(data["by_status"].keys()) == {"open", "in_progress", "escalated", "resolved", "closed"}
|
||||||
|
assert set(data["by_priority"].keys()) == {"low", "medium", "high", "critical"}
|
||||||
|
assert "total" in data
|
||||||
|
assert "active" in data
|
||||||
|
|||||||
Reference in New Issue
Block a user