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
## 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
133 lines
4.9 KiB
Python
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
|