Files
plex-playlist/backend/tests/integration/test_api.py

134 lines
4.9 KiB
Python

"""Integration tests for API endpoints."""
from importlib import metadata
from typing import cast
from unittest.mock import AsyncMock
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from backend.main import app, compatibility_status, get_api_session
client = TestClient(app)
@pytest.mark.integration
class TestAPIIntegration:
"""Integration tests for API endpoints."""
def test_health_check(self) -> None:
"""Test API health check endpoint."""
healthy_session = cast("AsyncSession", AsyncMock(spec=AsyncSession))
healthy_session.execute = AsyncMock(return_value=1)
async def override_get_session():
"""Provide a healthy session dependency override for tests."""
yield healthy_session
app.dependency_overrides[get_api_session] = override_get_session
try:
with TestClient(app) as local_client:
response = local_client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy", "database": "connected"}
finally:
app.dependency_overrides.clear()
def test_health_check_db_unavailable(self) -> None:
"""Health endpoint should return unavailable when DB probe fails."""
unhealthy_session = cast("AsyncSession", AsyncMock(spec=AsyncSession))
unhealthy_session.execute = AsyncMock(
side_effect=SQLAlchemyError("database unavailable")
)
async def override_get_session():
"""Provide an unhealthy session dependency override for tests."""
yield unhealthy_session
app.dependency_overrides[get_api_session] = override_get_session
try:
with TestClient(app) as local_client:
response = local_client.get("/health")
assert response.status_code == 503
assert response.json() == {
"status": "unhealthy",
"database": "disconnected",
}
finally:
app.dependency_overrides.clear()
def test_root_endpoint(self) -> None:
"""Test root endpoint."""
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Plex Playlist Backend API"}
def test_startup_rejects_invalid_runtime_policy(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Startup should fail fast when compatibility policy is invalid."""
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "99.0")
with pytest.raises(RuntimeError), TestClient(app):
pass
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
def test_compatibility_endpoint_reports_policy_status(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Compatibility endpoint should expose runtime and pinning policy status."""
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14")
with TestClient(app) as local_client:
response = local_client.get("/compatibility")
assert response.status_code == 200
payload = response.json()
assert payload["ok"] is True
assert payload["required_python"] == "3.14"
assert "current_python" in payload
assert payload["python_policy_valid"] is True
assert "required_packages" in payload
assert payload["required_packages"]["fastapi"] == "0.120.2"
assert payload["required_packages"]["uvicorn"] == "0.38.0"
assert payload["package_errors"] == {}
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
def test_startup_rejects_malformed_required_python(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Startup should fail fast when Python policy is not strict major.minor."""
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14.1")
with pytest.raises(RuntimeError), TestClient(app):
pass
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
def test_compatibility_status_handles_missing_package_metadata(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Compatibility checks should handle missing package metadata gracefully."""
def raise_not_found(_: str) -> str:
raise metadata.PackageNotFoundError("fake")
monkeypatch.setattr("backend.main._installed_version", raise_not_found)
status = compatibility_status()
package_checks = status["package_checks"]
package_errors = status["package_errors"]
assert status["ok"] is False
assert isinstance(package_checks, dict)
assert isinstance(package_errors, dict)
assert package_checks["fastapi"] is False
assert package_checks["uvicorn"] is False
assert "fastapi" in package_errors
assert "uvicorn" in package_errors