Files
plex-playlist/backend/tests/test_basic.py

281 lines
8.9 KiB
Python
Raw Normal View History

"""Basic tests for the backend application."""
from importlib import metadata
from pathlib import Path
from typing import Any, cast
PP-11_Add_SQLAlchemy_async_engine_session (#67) ## Summary This PR upgrades backend typing tooling and finalizes quality cleanup for the SQLAlchemy async engine/session work on `PP-11_Add_SQLAlchemy_async_engine_session`. ## What Changed - Upgraded pinned `pyright` version from `1.1.406` to `1.1.410`. - Regenerated backend lock state to align with the new pyright pin. - Removed deprecated `pythonPath` from pyright config to eliminate the config notice. - Refined backend DB runtime state handling and cleanup paths. - Updated database tests to avoid brittle singleton assumptions and keep typeguard-compatible async engine test doubles. ## Why - Remove tooling noise and keep checks deterministic. - Keep backend static/type/doc/test quality gates warning-free. - Improve reliability and clarity of DB engine/session lifecycle tests. ## Validation - `uv run ruff format --check .` - `uv run ruff check .` - `uv run pyright .` - `uv run pydoclint --config=pyproject.toml src/` - `uv run xdoctest --module backend` - `uv run pytest` Results: - Pyright: `0 errors, 0 warnings` - Pytest: `21 passed` ## Impact - No API contract changes intended. - No frontend changes. - Backend behavior remains the same; this is tooling/test/runtime-state cleanup. ## Risk - Low risk. - Main touched areas are backend tooling config, lockfile, DB runtime state internals, and tests. ## Checklist - [x] Backend lint clean - [x] Backend type checks clean - [x] Backend doc checks clean - [x] Backend tests passing - [x] Branch pushed and ready for review Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: https://dogar.darkhelm.org/DarkHelm.org/plex-playlist/pulls/67
2026-06-19 07:39:56 -04:00
from unittest.mock import AsyncMock
import pytest
PP-11_Add_SQLAlchemy_async_engine_session (#67) ## Summary This PR upgrades backend typing tooling and finalizes quality cleanup for the SQLAlchemy async engine/session work on `PP-11_Add_SQLAlchemy_async_engine_session`. ## What Changed - Upgraded pinned `pyright` version from `1.1.406` to `1.1.410`. - Regenerated backend lock state to align with the new pyright pin. - Removed deprecated `pythonPath` from pyright config to eliminate the config notice. - Refined backend DB runtime state handling and cleanup paths. - Updated database tests to avoid brittle singleton assumptions and keep typeguard-compatible async engine test doubles. ## Why - Remove tooling noise and keep checks deterministic. - Keep backend static/type/doc/test quality gates warning-free. - Improve reliability and clarity of DB engine/session lifecycle tests. ## Validation - `uv run ruff format --check .` - `uv run ruff check .` - `uv run pyright .` - `uv run pydoclint --config=pyproject.toml src/` - `uv run xdoctest --module backend` - `uv run pytest` Results: - Pyright: `0 errors, 0 warnings` - Pytest: `21 passed` ## Impact - No API contract changes intended. - No frontend changes. - Backend behavior remains the same; this is tooling/test/runtime-state cleanup. ## Risk - Low risk. - Main touched areas are backend tooling config, lockfile, DB runtime state internals, and tests. ## Checklist - [x] Backend lint clean - [x] Backend type checks clean - [x] Backend doc checks clean - [x] Backend tests passing - [x] Branch pushed and ready for review Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: https://dogar.darkhelm.org/DarkHelm.org/plex-playlist/pulls/67
2026-06-19 07:39:56 -04:00
from fastapi.testclient import TestClient
from sqlalchemy.exc import SQLAlchemyError
PP-11_Add_SQLAlchemy_async_engine_session (#67) ## Summary This PR upgrades backend typing tooling and finalizes quality cleanup for the SQLAlchemy async engine/session work on `PP-11_Add_SQLAlchemy_async_engine_session`. ## What Changed - Upgraded pinned `pyright` version from `1.1.406` to `1.1.410`. - Regenerated backend lock state to align with the new pyright pin. - Removed deprecated `pythonPath` from pyright config to eliminate the config notice. - Refined backend DB runtime state handling and cleanup paths. - Updated database tests to avoid brittle singleton assumptions and keep typeguard-compatible async engine test doubles. ## Why - Remove tooling noise and keep checks deterministic. - Keep backend static/type/doc/test quality gates warning-free. - Improve reliability and clarity of DB engine/session lifecycle tests. ## Validation - `uv run ruff format --check .` - `uv run ruff check .` - `uv run pyright .` - `uv run pydoclint --config=pyproject.toml src/` - `uv run xdoctest --module backend` - `uv run pytest` Results: - Pyright: `0 errors, 0 warnings` - Pytest: `21 passed` ## Impact - No API contract changes intended. - No frontend changes. - Backend behavior remains the same; this is tooling/test/runtime-state cleanup. ## Risk - Low risk. - Main touched areas are backend tooling config, lockfile, DB runtime state internals, and tests. ## Checklist - [x] Backend lint clean - [x] Backend type checks clean - [x] Backend doc checks clean - [x] Backend tests passing - [x] Branch pushed and ready for review Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: https://dogar.darkhelm.org/DarkHelm.org/plex-playlist/pulls/67
2026-06-19 07:39:56 -04:00
from sqlalchemy.ext.asyncio import AsyncSession
from backend import main as backend_main
from backend.main import (
app,
compatibility_check,
compatibility_status,
get_api_session,
read_root,
validate_runtime_policy,
)
parse_exact_pin = backend_main.__dict__["_parse_exact_pin"]
required_package_pins = backend_main.__dict__["_required_package_pins"]
required_package_pins_from_dependencies = backend_main.__dict__[
"_required_package_pins_from_dependencies"
]
def test_app_creation():
"""Test that the FastAPI app is created properly."""
assert app is not None
assert app.title == "Plex Playlist Backend"
assert app.version == "0.1.0"
def test_read_root():
"""Test the root endpoint function."""
result = read_root()
assert result == {"message": "Plex Playlist Backend API"}
def test_health_check():
"""Test the health check endpoint function."""
PP-11_Add_SQLAlchemy_async_engine_session (#67) ## Summary This PR upgrades backend typing tooling and finalizes quality cleanup for the SQLAlchemy async engine/session work on `PP-11_Add_SQLAlchemy_async_engine_session`. ## What Changed - Upgraded pinned `pyright` version from `1.1.406` to `1.1.410`. - Regenerated backend lock state to align with the new pyright pin. - Removed deprecated `pythonPath` from pyright config to eliminate the config notice. - Refined backend DB runtime state handling and cleanup paths. - Updated database tests to avoid brittle singleton assumptions and keep typeguard-compatible async engine test doubles. ## Why - Remove tooling noise and keep checks deterministic. - Keep backend static/type/doc/test quality gates warning-free. - Improve reliability and clarity of DB engine/session lifecycle tests. ## Validation - `uv run ruff format --check .` - `uv run ruff check .` - `uv run pyright .` - `uv run pydoclint --config=pyproject.toml src/` - `uv run xdoctest --module backend` - `uv run pytest` Results: - Pyright: `0 errors, 0 warnings` - Pytest: `21 passed` ## Impact - No API contract changes intended. - No frontend changes. - Backend behavior remains the same; this is tooling/test/runtime-state cleanup. ## Risk - Low risk. - Main touched areas are backend tooling config, lockfile, DB runtime state internals, and tests. ## Checklist - [x] Backend lint clean - [x] Backend type checks clean - [x] Backend doc checks clean - [x] Backend tests passing - [x] Branch pushed and ready for review Co-authored-by: copilotcoder <copilotcoder@darkhelm.org> Reviewed-on: https://dogar.darkhelm.org/DarkHelm.org/plex-playlist/pulls/67
2026-06-19 07:39:56 -04:00
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 client:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy", "database": "connected"}
finally:
app.dependency_overrides.clear()
def test_typeguard_validation():
"""Test that typeguard is working for type validation."""
# Test a function with type hints to ensure typeguard is active
def typed_function(x: int, y: str) -> str:
return f"{x}: {y}"
# This should work fine
result = typed_function(42, "test")
assert result == "42: test"
# This would fail with typeguard active, but we'll just test the happy path
# to ensure coverage of our code without breaking the test
def test_health_check_db_unavailable():
"""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 client:
response = client.get("/health")
assert response.status_code == 503
assert response.json() == {
"status": "unhealthy",
"database": "disconnected",
}
finally:
app.dependency_overrides.clear()
def test_compatibility_status_success(monkeypatch: pytest.MonkeyPatch):
"""Compatibility status should be healthy when all checks match policy."""
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14")
required_pins = cast("dict[str, str]", compatibility_status()["required_packages"])
def installed_version(package_name: str) -> str:
return required_pins[package_name]
monkeypatch.setattr("backend.main._installed_version", installed_version)
status = cast("dict[str, Any]", compatibility_status())
assert status["ok"] is True
assert status["package_errors"] == {}
def test_compatibility_status_missing_package(monkeypatch: pytest.MonkeyPatch):
"""Compatibility status should record metadata lookup failures."""
def missing_version(_: str) -> str:
raise metadata.PackageNotFoundError("missing")
monkeypatch.setattr("backend.main._installed_version", missing_version)
status = cast("dict[str, Any]", compatibility_status())
package_checks = cast("dict[str, bool]", status["package_checks"])
package_errors = cast("dict[str, str]", status["package_errors"])
assert status["ok"] is False
assert package_checks["fastapi"] is False
assert "fastapi" in package_errors
def test_validate_runtime_policy_raises(monkeypatch: pytest.MonkeyPatch):
"""Runtime policy validation should fail when compatibility is not ok."""
def invalid_status() -> dict[str, object]:
return {"ok": False}
monkeypatch.setattr("backend.main.compatibility_status", invalid_status)
with pytest.raises(RuntimeError):
validate_runtime_policy()
def test_compatibility_check_returns_status() -> None:
"""Compatibility endpoint helper should return policy payload."""
payload = compatibility_check()
assert "ok" in payload
def test_parse_exact_pin_handles_valid_and_invalid_requirements() -> None:
"""Exact pin parsing should normalize extras and reject malformed pins."""
assert parse_exact_pin("psycopg[binary]==3.2.12 ; python_version >= '3.14'") == (
"psycopg",
"3.2.12",
)
assert parse_exact_pin("uvicorn>=0.51.0") is None
assert parse_exact_pin("==0.51.0") is None
assert parse_exact_pin("uvicorn==") is None
def test_required_package_pins_from_dependencies_filters_non_exact_pins() -> None:
"""Only exact dependency pins should be included in runtime policy."""
dependencies = [
"fastapi==0.139.0",
"uvicorn>=0.51.0",
"psycopg[binary]==3.2.12",
]
assert required_package_pins_from_dependencies(dependencies) == {
"fastapi": "0.139.0",
"psycopg": "3.2.12",
}
def test_required_package_pins_uses_installed_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Installed project metadata should be the primary pin source."""
def fake_requires(_: str) -> list[str]:
return ["fastapi==0.139.0", "psycopg[binary]==3.2.12"]
def unexpected_read_text(_: Path, encoding: str = "utf-8") -> str:
del encoding
raise AssertionError("pyproject fallback should not run")
monkeypatch.setattr(backend_main.metadata, "requires", fake_requires)
monkeypatch.setattr(Path, "read_text", unexpected_read_text)
assert required_package_pins() == {
"fastapi": "0.139.0",
"psycopg": "3.2.12",
}
def test_required_package_pins_falls_back_to_pyproject(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Pyproject should be used when installed metadata is unavailable or unusable."""
def missing_requires(_: str) -> list[str]:
raise metadata.PackageNotFoundError("missing")
def fake_read_text(_: Path, encoding: str = "utf-8") -> str:
del encoding
return "[project]\ndependencies = []\n"
def fake_toml_loads(_: str) -> dict[str, object]:
return {
"project": {
"dependencies": [
"fastapi==0.139.0",
123,
"psycopg[binary]==3.2.12",
]
}
}
monkeypatch.setattr(backend_main.metadata, "requires", missing_requires)
monkeypatch.setattr(Path, "read_text", fake_read_text)
monkeypatch.setattr(backend_main.tomllib, "loads", fake_toml_loads)
assert required_package_pins() == {
"fastapi": "0.139.0",
"psycopg": "3.2.12",
}
def test_required_package_pins_handles_invalid_pyproject_shapes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Invalid pyproject structures should safely produce no required pins."""
def missing_requires(_: str) -> list[str]:
raise metadata.PackageNotFoundError("missing")
def fake_read_text(_: Path, encoding: str = "utf-8") -> str:
del encoding
return "[project]\ndependencies = []\n"
def invalid_project_loads(_: str) -> dict[str, object]:
return {"project": []}
def missing_dependencies_loads(_: str) -> dict[str, object]:
return {"project": {}}
monkeypatch.setattr(backend_main.metadata, "requires", missing_requires)
monkeypatch.setattr(Path, "read_text", fake_read_text)
monkeypatch.setattr(backend_main.tomllib, "loads", invalid_project_loads)
assert required_package_pins() == {}
monkeypatch.setattr(backend_main.tomllib, "loads", missing_dependencies_loads)
assert required_package_pins() == {}
def test_compatibility_status_rejects_malformed_required_python(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Malformed Python policy strings should fail compatibility checks."""
def empty_required_package_pins() -> dict[str, str]:
return {}
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14.1")
monkeypatch.setattr(
backend_main,
"_required_package_pins",
empty_required_package_pins,
)
status = cast("dict[str, Any]", compatibility_status())
assert status["ok"] is False
assert status["python_policy_valid"] is False