refactor(backend): separate db session errors from API translation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user