feat: comprehensive health check with DB, scheduler, cache
- Add /api/v1/health endpoint with component-level checks - Verify DB connectivity, fail2ban socket, scheduler, session cache - Add SQLite WAL cleanup on startup (orphan crash files) - Migration 8: import_log.timestamp → INTEGER UNIX epoch - Align import_log timestamps with history_archive (already UNIX int) - Add unit tests for DB cleanup and health router Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,15 +8,14 @@ from app.models.server import ServerStatus
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_200_when_online(client: AsyncClient) -> None:
|
||||
"""``GET /api/health`` must return HTTP 200 when fail2ban is online."""
|
||||
client._transport.app.state.server_status = ServerStatus(online=True)
|
||||
"""``GET /api/v1/health`` must return HTTP 200 when fail2ban is online."""
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_503_when_offline(client: AsyncClient) -> None:
|
||||
"""``GET /api/health`` must return HTTP 503 when fail2ban is offline."""
|
||||
"""``GET /api/v1/health`` must return HTTP 503 when fail2ban is offline."""
|
||||
client._transport.app.state.server_status = ServerStatus(online=False)
|
||||
response = await client.get("/api/v1/health")
|
||||
assert response.status_code == 503
|
||||
@@ -24,27 +23,84 @@ async def test_health_check_returns_503_when_offline(client: AsyncClient) -> Non
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_ok_status_when_online(client: AsyncClient) -> None:
|
||||
"""``GET /api/health`` must contain ``status: ok`` when fail2ban is online."""
|
||||
client._transport.app.state.server_status = ServerStatus(online=True)
|
||||
"""``GET /api/v1/health`` must contain ``status: ok`` when fail2ban is online."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, str] = response.json()
|
||||
data: dict[str, object] = response.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["fail2ban"] == "online"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_unavailable_when_offline(client: AsyncClient) -> None:
|
||||
"""``GET /api/health`` must contain ``status: unavailable`` when fail2ban is offline."""
|
||||
"""``GET /api/v1/health`` must contain ``status: unavailable`` when fail2ban is offline."""
|
||||
client._transport.app.state.server_status = ServerStatus(online=False)
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, str] = response.json()
|
||||
data: dict[str, object] = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
assert data["fail2ban"] == "offline"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_content_type_is_json(client: AsyncClient) -> None:
|
||||
"""``GET /api/health`` must set the ``Content-Type`` header to JSON."""
|
||||
"""``GET /api/v1/health`` must set the ``Content-Type`` header to JSON."""
|
||||
response = await client.get("/api/v1/health")
|
||||
assert "application/json" in response.headers.get("content-type", "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_includes_database_status(client: AsyncClient) -> None:
|
||||
"""``GET /api/v1/health`` must include database status field."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert "database" in data
|
||||
assert data["database"] in ("ok", "error")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_includes_scheduler_status(client: AsyncClient) -> None:
|
||||
"""``GET /api/v1/health`` must include scheduler status field."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert "scheduler" in data
|
||||
assert data["scheduler"] in ("running", "stopped", "unknown")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_includes_cache_status(client: AsyncClient) -> None:
|
||||
"""``GET /api/v1/health`` must include cache status field."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert "cache" in data
|
||||
assert data["cache"] in ("initialised", "uninitialised")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_includes_components_list(client: AsyncClient) -> None:
|
||||
"""``GET /api/v1/health`` must include components list."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert "components" in data
|
||||
assert isinstance(data["components"], list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_offline_adds_fail2ban_to_components(
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
"""When fail2ban is offline, it must appear in the components list."""
|
||||
client._transport.app.state.server_status = ServerStatus(online=False)
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert data["status"] == "unavailable"
|
||||
components: list[dict[str, object]] = data["components"] # type: ignore[assignment]
|
||||
assert any(c.get("name") == "fail2ban" and c.get("healthy") is False for c in components)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_online_returns_empty_components(client: AsyncClient) -> None:
|
||||
"""When all components are healthy, components list must be empty."""
|
||||
response = await client.get("/api/v1/health")
|
||||
data: dict[str, object] = response.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["components"] == []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user