"""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"