diff --git a/backend/src/backend/database.py b/backend/src/backend/database.py index 24224eb..55f85b1 100644 --- a/backend/src/backend/database.py +++ b/backend/src/backend/database.py @@ -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 diff --git a/backend/src/backend/main.py b/backend/src/backend/main.py index 719c735..2b2ebda 100644 --- a/backend/src/backend/main.py +++ b/backend/src/backend/main.py @@ -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): diff --git a/backend/tests/integration/test_api.py b/backend/tests/integration/test_api.py index 5294361..176ea23 100644 --- a/backend/tests/integration/test_api.py +++ b/backend/tests/integration/test_api.py @@ -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") diff --git a/backend/tests/test_basic.py b/backend/tests/test_basic.py index 6f8ebae..06499cf 100644 --- a/backend/tests/test_basic.py +++ b/backend/tests/test_basic.py @@ -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") diff --git a/backend/tests/test_database.py b/backend/tests/test_database.py index 0bb87fc..7b8a6c7 100644 --- a/backend/tests/test_database.py +++ b/backend/tests/test_database.py @@ -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(