refactor(backend): separate db session errors from API translation

This commit is contained in:
copilotcoder
2026-06-18 14:10:30 -04:00
parent 6a36ea7e96
commit 22d8677fbd
5 changed files with 29 additions and 28 deletions

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import os
from typing import TYPE_CHECKING
from fastapi import HTTPException, status
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import (
@@ -77,17 +76,8 @@ async def get_session() -> AsyncIterator[AsyncSession]:
Yields:
Async session scoped to the current request lifecycle.
Raises:
HTTPException: If database configuration is missing.
"""
try:
session_factory = get_sessionmaker()
except DatabaseConfigurationError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail={"status": "unhealthy", "database": "not_configured"},
) from exc
session_factory = get_sessionmaker()
async with session_factory() as session:
yield session

View File

@@ -10,10 +10,15 @@ from contextlib import asynccontextmanager
from importlib import metadata
from typing import Any
from fastapi import Depends, FastAPI, status
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
from backend.database import dispose_engine, get_session, probe_database
from backend.database import (
DatabaseConfigurationError,
dispose_engine,
get_session,
probe_database,
)
REQUIRED_PACKAGE_PINS: dict[str, str] = {
"fastapi": "0.120.2",
@@ -110,9 +115,21 @@ def read_root() -> dict[str, str]:
return {"message": "Plex Playlist Backend API"}
async def get_api_session() -> Any:
"""Yield a DB session for API handlers, translating config errors to HTTP 503."""
try:
async for session in get_session():
yield session
except DatabaseConfigurationError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail={"status": "unhealthy", "database": "not_configured"},
) from exc
@app.get("/health")
async def health_check(
session: Any = Depends(get_session), # pyright: ignore[reportCallInDefaultInitializer]
session: Any = Depends(get_api_session), # pyright: ignore[reportCallInDefaultInitializer]
) -> JSONResponse:
"""Health check endpoint with database connectivity validation."""
if await probe_database(session):

View File

@@ -9,8 +9,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_session
from backend.main import app, compatibility_status
from backend.main import app, compatibility_status, get_api_session
client = TestClient(app)
@@ -28,7 +27,7 @@ class TestAPIIntegration:
"""Provide a healthy session dependency override for tests."""
yield healthy_session
app.dependency_overrides[get_session] = override_get_session
app.dependency_overrides[get_api_session] = override_get_session
try:
with TestClient(app) as local_client:
response = local_client.get("/health")
@@ -49,7 +48,7 @@ class TestAPIIntegration:
"""Provide an unhealthy session dependency override for tests."""
yield unhealthy_session
app.dependency_overrides[get_session] = override_get_session
app.dependency_overrides[get_api_session] = override_get_session
try:
with TestClient(app) as local_client:
response = local_client.get("/health")

View File

@@ -6,8 +6,7 @@ from unittest.mock import AsyncMock
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_session
from backend.main import app, read_root
from backend.main import app, get_api_session, read_root
def test_app_creation():
@@ -33,7 +32,7 @@ def test_health_check():
"""Provide a healthy session dependency override for tests."""
yield healthy_session
app.dependency_overrides[get_session] = override_get_session
app.dependency_overrides[get_api_session] = override_get_session
try:
with TestClient(app) as client:
response = client.get("/health")

View File

@@ -4,7 +4,6 @@ from typing import Any, cast
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
@@ -80,10 +79,10 @@ def test_get_sessionmaker_is_singleton(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.asyncio
async def test_get_session_raises_503_when_db_not_configured(
async def test_get_session_raises_database_error_when_db_not_configured(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Session dependency should translate config errors to HTTP 503."""
"""Session helper should raise a database-layer config error when missing."""
def raise_config_error() -> Any:
raise database.DatabaseConfigurationError("missing")
@@ -91,12 +90,9 @@ async def test_get_session_raises_503_when_db_not_configured(
monkeypatch.setattr(database, "get_sessionmaker", raise_config_error)
generator = database.get_session()
with pytest.raises(HTTPException) as exc:
with pytest.raises(database.DatabaseConfigurationError):
await anext(generator)
assert exc.value.status_code == 503
assert exc.value.detail == {"status": "unhealthy", "database": "not_configured"}
@pytest.mark.asyncio
async def test_get_session_yields_scoped_session(