From e1e7c45c06a4674e182c3a7a55d048bd05574cee Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Sat, 16 May 2026 09:53:12 -0400 Subject: [PATCH] Harden runtime compatibility policy checks --- backend/src/backend/main.py | 36 +++++++++++++++++++------ backend/tests/integration/test_api.py | 38 ++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/backend/src/backend/main.py b/backend/src/backend/main.py index 86cb37a..79463cd 100644 --- a/backend/src/backend/main.py +++ b/backend/src/backend/main.py @@ -15,12 +15,22 @@ REQUIRED_PACKAGE_PINS: dict[str, str] = { } +def _parse_required_python(required_python: str) -> tuple[int, int] | None: + """Parse required Python version from strict major.minor format.""" + value = required_python.strip() + pieces = value.split(".") + if len(pieces) != 2 or not all(piece.isdigit() for piece in pieces): + return None + return int(pieces[0]), int(pieces[1]) + + def _runtime_meets_required_python(required_python: str) -> bool: - """Return True when the interpreter version satisfies required major.minor.""" - major_str, minor_str = required_python.split(".", maxsplit=1) - required = (int(major_str), int(minor_str)) + """Return True when the interpreter major.minor exactly matches required policy.""" + required = _parse_required_python(required_python) + if required is None: + return False current = (sys.version_info.major, sys.version_info.minor) - return current >= required + return current == required def _installed_version(package_name: str) -> str: @@ -32,22 +42,32 @@ def compatibility_status() -> dict[str, object]: """Compute runtime and pinned-dependency compatibility status.""" required_python = os.getenv("BACKEND_REQUIRED_PYTHON", "3.14") current_python = f"{sys.version_info.major}.{sys.version_info.minor}" + required_python_parsed = _parse_required_python(required_python) package_checks: dict[str, bool] = {} + package_errors: dict[str, str] = {} for package_name, required_version in REQUIRED_PACKAGE_PINS.items(): - package_checks[package_name] = ( - _installed_version(package_name) == required_version - ) + try: + package_checks[package_name] = ( + _installed_version(package_name) == required_version + ) + except metadata.PackageNotFoundError as exc: + package_checks[package_name] = False + package_errors[package_name] = str(exc) - runtime_ok = _runtime_meets_required_python(required_python) + runtime_ok = required_python_parsed is not None and _runtime_meets_required_python( + required_python + ) packages_ok = all(package_checks.values()) return { "ok": runtime_ok and packages_ok, "required_python": required_python, "current_python": current_python, + "python_policy_valid": required_python_parsed is not None, "required_packages": REQUIRED_PACKAGE_PINS, "package_checks": package_checks, + "package_errors": package_errors, } diff --git a/backend/tests/integration/test_api.py b/backend/tests/integration/test_api.py index 4a65e43..9a27fe0 100644 --- a/backend/tests/integration/test_api.py +++ b/backend/tests/integration/test_api.py @@ -1,9 +1,11 @@ """Integration tests for API endpoints.""" +from importlib import metadata + import pytest from fastapi.testclient import TestClient -from backend.main import app +from backend.main import app, compatibility_status client = TestClient(app) @@ -49,8 +51,42 @@ class TestAPIIntegration: 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