Files
incidentops/tests/api/test_incidents.py

231 lines
7.4 KiB
Python
Raw Permalink Normal View History

"""Integration tests for incident endpoints."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from uuid import UUID, uuid4
import asyncpg
import pytest
from httpx import AsyncClient
from app.core import security
from app.repositories.incident import IncidentRepository
from tests.api import helpers
pytestmark = pytest.mark.asyncio
API_PREFIX = "/v1"
async def _create_service(conn: asyncpg.Connection, org_id: UUID, slug: str = "api") -> UUID:
service_id = uuid4()
await conn.execute(
"INSERT INTO services (id, org_id, name, slug) VALUES ($1, $2, $3, $4)",
service_id,
org_id,
"API",
slug,
)
return service_id
async def _create_incident(
conn: asyncpg.Connection,
org_id: UUID,
service_id: UUID,
title: str,
severity: str = "low",
created_at: datetime | None = None,
) -> UUID:
repo = IncidentRepository(conn)
incident_id = uuid4()
incident = await repo.create(
incident_id,
org_id,
service_id,
title,
description=None,
severity=severity,
)
if created_at:
await conn.execute(
"UPDATE incidents SET created_at = $1 WHERE id = $2",
created_at,
incident_id,
)
return incident["id"]
async def _login(client: AsyncClient, *, email: str, password: str) -> dict:
response = await client.post(
f"{API_PREFIX}/auth/login",
json={"email": email, "password": password},
)
response.raise_for_status()
return response.json()
async def test_create_incident_requires_member_role(
api_client: AsyncClient, db_admin: asyncpg.Connection
) -> None:
owner_tokens = await helpers.register_user(
api_client,
email="owner-inc@example.com",
password="OwnerInc1!",
org_name="Incident Org",
)
payload = security.decode_access_token(owner_tokens["access_token"])
org_id = UUID(payload["org_id"])
service_id = await _create_service(db_admin, org_id)
viewer_password = "Viewer123!"
viewer_id = uuid4()
await db_admin.execute(
"INSERT INTO users (id, email, password_hash) VALUES ($1, $2, $3)",
viewer_id,
"viewer@example.com",
security.hash_password(viewer_password),
)
await db_admin.execute(
"INSERT INTO org_members (id, user_id, org_id, role) VALUES ($1, $2, $3, $4)",
uuid4(),
viewer_id,
org_id,
"viewer",
)
viewer_tokens = await _login(api_client, email="viewer@example.com", password=viewer_password)
forbidden = await api_client.post(
f"{API_PREFIX}/services/{service_id}/incidents",
json={"title": "View only", "description": None, "severity": "low"},
headers={"Authorization": f"Bearer {viewer_tokens['access_token']}"},
)
assert forbidden.status_code == 403
created = await api_client.post(
f"{API_PREFIX}/services/{service_id}/incidents",
json={"title": "Database down", "description": "Primary unavailable", "severity": "critical"},
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
)
assert created.status_code == 201
incident_id = UUID(created.json()["id"])
row = await db_admin.fetchrow(
"SELECT status, org_id FROM incidents WHERE id = $1",
incident_id,
)
assert row is not None and row["status"] == "triggered" and row["org_id"] == org_id
event = await db_admin.fetchrow(
"SELECT event_type FROM incident_events WHERE incident_id = $1",
incident_id,
)
assert event is not None and event["event_type"] == "created"
async def test_list_incidents_paginates_and_isolates_org(
api_client: AsyncClient, db_admin: asyncpg.Connection
) -> None:
tokens = await helpers.register_user(
api_client,
email="pager@example.com",
password="Pager123!",
org_name="Pager Org",
)
payload = security.decode_access_token(tokens["access_token"])
org_id = UUID(payload["org_id"])
service_id = await _create_service(db_admin, org_id)
now = datetime.now(UTC)
await _create_incident(db_admin, org_id, service_id, "Old", created_at=now - timedelta(minutes=3))
await _create_incident(db_admin, org_id, service_id, "Mid", created_at=now - timedelta(minutes=2))
await _create_incident(db_admin, org_id, service_id, "New", created_at=now - timedelta(minutes=1))
# Noise in another org
other_org = await helpers.create_org(db_admin, name="Other", slug="other")
other_service = await _create_service(db_admin, other_org, slug="other-api")
await _create_incident(db_admin, other_org, other_service, "Other incident")
response = await api_client.get(
f"{API_PREFIX}/incidents",
params={"limit": 2},
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert response.status_code == 200
body = response.json()
titles = [item["title"] for item in body["items"]]
assert titles == ["New", "Mid"]
assert body["has_more"] is True
assert body["next_cursor"] is not None
async def test_transition_incident_enforces_version_and_updates_status(
api_client: AsyncClient, db_admin: asyncpg.Connection
) -> None:
tokens = await helpers.register_user(
api_client,
email="trans@example.com",
password="Trans123!",
org_name="Trans Org",
)
payload = security.decode_access_token(tokens["access_token"])
org_id = UUID(payload["org_id"])
service_id = await _create_service(db_admin, org_id)
incident_id = await _create_incident(db_admin, org_id, service_id, "Queue backlog")
conflict = await api_client.post(
f"{API_PREFIX}/incidents/{incident_id}/transition",
json={"to_status": "acknowledged", "version": 5, "note": None},
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert conflict.status_code == 409
ok = await api_client.post(
f"{API_PREFIX}/incidents/{incident_id}/transition",
json={"to_status": "acknowledged", "version": 1, "note": "Looking"},
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert ok.status_code == 200
assert ok.json()["status"] == "acknowledged"
assert ok.json()["version"] == 2
async def test_add_comment_appends_event(
api_client: AsyncClient, db_admin: asyncpg.Connection
) -> None:
tokens = await helpers.register_user(
api_client,
email="commenter@example.com",
password="Commenter1!",
org_name="Comment Org",
)
payload = security.decode_access_token(tokens["access_token"])
org_id = UUID(payload["org_id"])
service_id = await _create_service(db_admin, org_id)
incident_id = await _create_incident(db_admin, org_id, service_id, "Add comment")
response = await api_client.post(
f"{API_PREFIX}/incidents/{incident_id}/comment",
json={"content": "Monitoring"},
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert response.status_code == 201
body = response.json()
assert body["event_type"] == "comment_added"
assert body["payload"] == {"content": "Monitoring"}
event_row = await db_admin.fetchrow(
"SELECT event_type, actor_user_id FROM incident_events WHERE id = $1",
UUID(body["id"]),
)
assert event_row is not None
assert event_row["event_type"] == "comment_added"