Files
plex-playlist/backend/tests/integration/test_api.py
Xlorep DarkHelm f3698b095b
All checks were successful
CICD / Build and Publish CICD Base Image (push) Successful in 6m8s
CICD / Build and Push CICD Image (push) Successful in 23m14s
CICD / Build CICD Image Failure Postmortem (push) Has been skipped
CICD / Backend Tests (push) Successful in 7m10s
CICD / Frontend Tests (push) Successful in 45s
CICD / Backend Doctests (push) Successful in 18s
CICD / Pre-commit Checks (push) Successful in 14m57s
CICD / Source Lanes Failure Postmortem (push) Has been skipped
CICD / CICD Tests Complete (push) Successful in 3s
CICD / Build Backend Base Image (push) Successful in 18s
CICD / Build Integration Tester Image (push) Successful in 1m5s
CICD / Build Backend Main Image (push) Successful in 1m52s
CICD / Build Frontend Base Image (push) Successful in 10m42s
CICD / Build Frontend Main Image (push) Successful in 33s
CICD / Build E2E Tester Image (push) Successful in 32m17s
CICD / Production Images Complete (push) Successful in 5s
CICD / Production Image Failures Postmortem (push) Has been skipped
CICD / Runtime Black-Box Integration Tests (push) Successful in 1m13s
CICD / Integration Tests Failure Postmortem (push) Has been skipped
CICD / End-to-End Tests (push) Successful in 11m23s
CICD / E2E Tests Failure Postmortem (push) Has been skipped
Stabilize self-hosted CI workflows and resolve issue #62 (#73)
## Summary

Hardens CI workflows for self-hosted Gitea runners by stabilizing E2E execution and Renovate behavior across internal/external network paths.

Closes #62

## What Changed

### E2E workflow reliability
- Fixed E2E workspace handoff to ensure expected repository contents are present during test execution.
- Added stricter preflight checks for required frontend files before running E2E.
- Reduced mount/path fragility while preserving runtime image pull and compose flow.

### Renovate workflow hardening
- Added internal-first endpoint reachability selection with fallback handling.
- Added token preflight checks for repository access.
- Added explicit host-rule auth handling for API/git paths.
- Added container-level connectivity preflight diagnostics.
- Added git URL override aligned with selected endpoint context.
- Removed incorrect forced Dogar host-IP pinning that broke HTTPS clone routing.

## Why

CI behavior was sensitive to runner networking and Renovate clone/auth interactions. These changes make the workflow deterministic in our runner topology and address recurring CI failures.

## Scope

- Workflow logic only (`cicd.yaml`, `renovate.yml`)
- No app feature or API behavior changes

## Validation

- Workflow YAML validation passed during updates.
- Changes were applied and verified iteratively from real failing run diagnostics.

Co-authored-by: copilotcoder <copilotcoder@darkhelm.org>
Reviewed-on: #73
2026-07-13 11:16:16 -04:00

133 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)
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