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
+1 -11
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from fastapi import HTTPException, status
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
@@ -77,17 +76,8 @@ async def get_session() -> AsyncIterator[AsyncSession]:
Yields: Yields:
Async session scoped to the current request lifecycle. Async session scoped to the current request lifecycle.
Raises:
HTTPException: If database configuration is missing.
""" """
try: session_factory = get_sessionmaker()
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
async with session_factory() as session: async with session_factory() as session:
yield session yield session
+20 -3
View File
@@ -10,10 +10,15 @@ from contextlib import asynccontextmanager
from importlib import metadata from importlib import metadata
from typing import Any from typing import Any
from fastapi import Depends, FastAPI, status from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.responses import JSONResponse 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] = { REQUIRED_PACKAGE_PINS: dict[str, str] = {
"fastapi": "0.120.2", "fastapi": "0.120.2",
@@ -110,9 +115,21 @@ def read_root() -> dict[str, str]:
return {"message": "Plex Playlist Backend API"} 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") @app.get("/health")
async def health_check( async def health_check(
session: Any = Depends(get_session), # pyright: ignore[reportCallInDefaultInitializer] session: Any = Depends(get_api_session), # pyright: ignore[reportCallInDefaultInitializer]
) -> JSONResponse: ) -> JSONResponse:
"""Health check endpoint with database connectivity validation.""" """Health check endpoint with database connectivity validation."""
if await probe_database(session): if await probe_database(session):
+3 -4
View File
@@ -9,8 +9,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_session from backend.main import app, compatibility_status, get_api_session
from backend.main import app, compatibility_status
client = TestClient(app) client = TestClient(app)
@@ -28,7 +27,7 @@ class TestAPIIntegration:
"""Provide a healthy session dependency override for tests.""" """Provide a healthy session dependency override for tests."""
yield healthy_session yield healthy_session
app.dependency_overrides[get_session] = override_get_session app.dependency_overrides[get_api_session] = override_get_session
try: try:
with TestClient(app) as local_client: with TestClient(app) as local_client:
response = local_client.get("/health") response = local_client.get("/health")
@@ -49,7 +48,7 @@ class TestAPIIntegration:
"""Provide an unhealthy session dependency override for tests.""" """Provide an unhealthy session dependency override for tests."""
yield unhealthy_session yield unhealthy_session
app.dependency_overrides[get_session] = override_get_session app.dependency_overrides[get_api_session] = override_get_session
try: try:
with TestClient(app) as local_client: with TestClient(app) as local_client:
response = local_client.get("/health") response = local_client.get("/health")
+2 -3
View File
@@ -6,8 +6,7 @@ from unittest.mock import AsyncMock
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_session from backend.main import app, get_api_session, read_root
from backend.main import app, read_root
def test_app_creation(): def test_app_creation():
@@ -33,7 +32,7 @@ def test_health_check():
"""Provide a healthy session dependency override for tests.""" """Provide a healthy session dependency override for tests."""
yield healthy_session yield healthy_session
app.dependency_overrides[get_session] = override_get_session app.dependency_overrides[get_api_session] = override_get_session
try: try:
with TestClient(app) as client: with TestClient(app) as client:
response = client.get("/health") response = client.get("/health")
+3 -7
View File
@@ -4,7 +4,6 @@ from typing import Any, cast
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest import pytest
from fastapi import HTTPException
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
@@ -80,10 +79,10 @@ def test_get_sessionmaker_is_singleton(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.asyncio @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, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> 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: def raise_config_error() -> Any:
raise database.DatabaseConfigurationError("missing") 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) monkeypatch.setattr(database, "get_sessionmaker", raise_config_error)
generator = database.get_session() generator = database.get_session()
with pytest.raises(HTTPException) as exc: with pytest.raises(database.DatabaseConfigurationError):
await anext(generator) await anext(generator)
assert exc.value.status_code == 503
assert exc.value.detail == {"status": "unhealthy", "database": "not_configured"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_session_yields_scoped_session( async def test_get_session_yields_scoped_session(