refactor: engineering improvements -- API versioning, structured logging, Alembic, error standardization, test coverage
- API versioning: all REST endpoints prefixed with /api/v1/ - Structured logging: replaced stdlib logging with structlog (console/JSON modes) - Alembic migrations: versioned DB schema with initial migration - Error standardization: global exception handlers for consistent envelope format - Interrupt cleanup: asyncio background task for expired interrupt removal - Integration tests: +30 tests (analytics, replay, openapi, error, session APIs) - Frontend tests: +57 tests (all components, pages, useWebSocket hook) - Backend: 557 tests, 89.75% coverage | Frontend: 80 tests, 16 test files
This commit is contained in:
@@ -5,9 +5,12 @@ from __future__ import annotations
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api_utils import envelope
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
@@ -16,6 +19,14 @@ def _build_app() -> FastAPI:
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def _http_exc(request, exc): # type: ignore[no-untyped-def]
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=envelope(None, success=False, error=exc.detail),
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -64,7 +75,7 @@ class TestListConversations:
|
||||
app.state.pool = _make_mock_pool([], count=0)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations")
|
||||
resp = client.get("/api/v1/conversations")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
@@ -89,7 +100,7 @@ class TestListConversations:
|
||||
app.state.pool = _make_mock_pool(mock_rows, count=1)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations")
|
||||
resp = client.get("/api/v1/conversations")
|
||||
body = resp.json()
|
||||
assert resp.status_code == 200
|
||||
data = body["data"]
|
||||
@@ -102,7 +113,7 @@ class TestListConversations:
|
||||
app.state.pool = _make_mock_pool([], count=0)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations")
|
||||
resp = client.get("/api/v1/conversations")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_pagination_custom_params(self) -> None:
|
||||
@@ -110,7 +121,7 @@ class TestListConversations:
|
||||
app.state.pool = _make_mock_pool([], count=0)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations?page=2&per_page=10")
|
||||
resp = client.get("/api/v1/conversations?page=2&per_page=10")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_per_page_max_capped_at_100(self) -> None:
|
||||
@@ -118,7 +129,7 @@ class TestListConversations:
|
||||
app.state.pool = _make_mock_pool([], count=0)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations?per_page=200")
|
||||
resp = client.get("/api/v1/conversations?per_page=200")
|
||||
# FastAPI Query(le=100) rejects values > 100
|
||||
assert resp.status_code == 422
|
||||
|
||||
@@ -129,7 +140,7 @@ class TestGetReplay:
|
||||
app.state.pool = _make_mock_pool([])
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/nonexistent-thread")
|
||||
resp = client.get("/api/v1/replay/nonexistent-thread")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_returns_replay_page_for_existing_thread(self) -> None:
|
||||
@@ -149,7 +160,7 @@ class TestGetReplay:
|
||||
app.state.pool = _make_mock_pool(mock_rows)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/thread-123")
|
||||
resp = client.get("/api/v1/replay/thread-123")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
@@ -174,7 +185,7 @@ class TestGetReplay:
|
||||
app.state.pool = _make_mock_pool(mock_rows)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/t1?page=1&per_page=5")
|
||||
resp = client.get("/api/v1/replay/t1?page=1&per_page=5")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_error_response_has_envelope(self) -> None:
|
||||
@@ -182,16 +193,19 @@ class TestGetReplay:
|
||||
app.state.pool = _make_mock_pool([])
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/missing")
|
||||
resp = client.get("/api/v1/replay/missing")
|
||||
assert resp.status_code == 404
|
||||
assert "detail" in resp.json()
|
||||
body = resp.json()
|
||||
assert body["success"] is False
|
||||
assert body["data"] is None
|
||||
assert body["error"] is not None
|
||||
|
||||
def test_invalid_thread_id_returns_400(self) -> None:
|
||||
app = _build_app()
|
||||
app.state.pool = _make_mock_pool([])
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/id%20with%20spaces")
|
||||
resp = client.get("/api/v1/replay/id%20with%20spaces")
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_thread_id_special_chars_returns_400(self) -> None:
|
||||
@@ -199,5 +213,5 @@ class TestGetReplay:
|
||||
app.state.pool = _make_mock_pool([])
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/id;DROP TABLE")
|
||||
resp = client.get("/api/v1/replay/id;DROP TABLE")
|
||||
assert resp.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user