feat: indie status page MVP -- FastAPI + SQLite

- 8 DB models (services, incidents, monitors, subscribers, etc.)
- Full CRUD API for services, incidents, monitors
- Public status page with live data
- Incident detail page with timeline
- API key authentication
- Uptime monitoring scheduler
- 13 tests passing
- TECHNICAL_DESIGN.md with full spec
This commit is contained in:
IndieStatusBot 2026-04-25 05:00:00 +00:00
commit 902133edd3
4655 changed files with 1342691 additions and 0 deletions

154
tests/test_api_incidents.py Normal file
View file

@ -0,0 +1,154 @@
"""Test Incidents API endpoints."""
import pytest
@pytest.mark.asyncio
async def test_create_incident(client, api_key):
"""Should create a new incident after creating a service."""
# Create a service first
svc = await client.post(
"/api/v1/services/",
json={"name": "Auth Service", "slug": "auth-service"},
headers={"X-API-Key": api_key},
)
assert svc.status_code == 201
service_id = svc.json()["id"]
# Create an incident
response = await client.post(
"/api/v1/incidents/",
json={
"service_id": service_id,
"title": "Login failures",
"status": "investigating",
"severity": "major",
},
headers={"X-API-Key": api_key},
)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Login failures"
assert data["status"] == "investigating"
@pytest.mark.asyncio
async def test_list_incidents(client, api_key):
"""Should list incidents."""
response = await client.get("/api/v1/incidents/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_incident_with_updates(client, api_key):
"""Should get an incident and add an update."""
# Create a service
svc = await client.post(
"/api/v1/services/",
json={"name": "Storage", "slug": "storage"},
headers={"X-API-Key": api_key},
)
service_id = svc.json()["id"]
# Create an incident
inc = await client.post(
"/api/v1/incidents/",
json={
"service_id": service_id,
"title": "Data loss",
"status": "investigating",
"severity": "outage",
},
headers={"X-API-Key": api_key},
)
incident_id = inc.json()["id"]
# Add an update
upd = await client.post(
f"/api/v1/incidents/{incident_id}/updates",
json={
"status": "identified",
"body": "We found the root cause.",
},
headers={"X-API-Key": api_key},
)
assert upd.status_code == 201
assert upd.json()["status"] == "identified"
# Get the incident - should include updates
get_resp = await client.get(f"/api/v1/incidents/{incident_id}")
assert get_resp.status_code == 200
data = get_resp.json()
assert data["status"] == "identified"
assert len(data["updates"]) == 1
assert data["updates"][0]["body"] == "We found the root cause."
@pytest.mark.asyncio
async def test_update_incident(client, api_key):
"""Should update incident status via PATCH."""
# Create service + incident
svc = await client.post(
"/api/v1/services/",
json={"name": "Cache", "slug": "cache"},
headers={"X-API-Key": api_key},
)
service_id = svc.json()["id"]
inc = await client.post(
"/api/v1/incidents/",
json={
"service_id": service_id,
"title": "Slow responses",
"status": "investigating",
"severity": "minor",
},
headers={"X-API-Key": api_key},
)
incident_id = inc.json()["id"]
# Patch the incident
patch_resp = await client.patch(
f"/api/v1/incidents/{incident_id}",
json={"status": "resolved"},
headers={"X-API-Key": api_key},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["status"] == "resolved"
assert data["resolved_at"] is not None
@pytest.mark.asyncio
async def test_delete_incident(client, api_key):
"""Should delete an incident."""
svc = await client.post(
"/api/v1/services/",
json={"name": "Email", "slug": "email-svc"},
headers={"X-API-Key": api_key},
)
service_id = svc.json()["id"]
inc = await client.post(
"/api/v1/incidents/",
json={
"service_id": service_id,
"title": "Emails delayed",
"status": "monitoring",
"severity": "minor",
},
headers={"X-API-Key": api_key},
)
incident_id = inc.json()["id"]
del_resp = await client.delete(
f"/api/v1/incidents/{incident_id}",
headers={"X-API-Key": api_key},
)
assert del_resp.status_code == 204
# Verify it's gone
get_resp = await client.get(f"/api/v1/incidents/{incident_id}")
assert get_resp.status_code == 404