All checks were successful
CICD / Build and Push CICD Images (push) Successful in 41m21s
CICD / Build CICD Image Failure Postmortem (push) Has been skipped
CICD / Source Checks (push) Successful in 7m13s
CICD / Source Lanes Failure Postmortem (push) Has been skipped
CICD / Dependency Audits (Informational) (push) Successful in 7m37s
CICD / CICD Tests Complete (push) Successful in 10s
CICD / Build Tester Images (push) Successful in 3m45s
CICD / Build Release Images (push) Successful in 31m50s
CICD / Production Image Failures Postmortem (push) Has been skipped
CICD / Production Images Complete (push) Successful in 2s
CICD / Runtime Black-Box Integration Tests (push) Successful in 53s
CICD / Integration Tests Failure Postmortem (push) Has been skipped
CICD / End-to-End Tests (push) Successful in 3m17s
CICD / E2E Tests Failure Postmortem (push) Has been skipped
CICD / Promote Staging Images To Release (push) Successful in 2m4s
281 lines
8.9 KiB
Python
281 lines
8.9 KiB
Python
"""Basic tests for the backend application."""
|
|
|
|
from importlib import metadata
|
|
from pathlib import Path
|
|
from typing import Any, 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 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."""
|
|
|
|
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
|