thermograph/frontend/tests/conftest.py

123 lines
4.8 KiB
Python
Raw Permalink Normal View History

"""Shared SSR test setup — self-contained, NO backend checkout.
Replaces the old sibling-`backend/` coupling. Two tiers:
* unit (tests/unit/) hermetic. The `client` fixture serves the SSR app with
`api_client` monkeypatched to return committed fixtures (tests/fixtures/, refreshed
by scripts/capture-fixtures.sh). No backend, no Docker.
* integration (tests/integration/) the `live_client` fixture serves the SSR app with
`api_client` pointed at a REAL backend container (scripts/backend-for-tests.sh pulls
+ runs it). Skips automatically when Docker/the image isn't available.
"""
import json
import os
import subprocess
import sys
import httpx
import pytest
_HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(_HERE)
if REPO not in sys.path:
sys.path.insert(0, REPO) # SSR modules (content.py, app.py, api_client.py) live at repo root
# api_client raises at import if this is unset. Unit tests monkeypatch api_client so it is
# never used; the integration fixture overrides it to the live backend.
os.environ.setdefault("THERMOGRAPH_API_BASE_INTERNAL", "http://backend.invalid")
os.environ.setdefault("THERMOGRAPH_BASE", "/thermograph")
import content # noqa: E402 — imports api_client under the env set above
B = content.BASE # "/thermograph"
SLUG = "london-england-gb" # captured into tests/fixtures/ (GB -> Celsius)
MONTH = "july"
FAKE_INDEXNOW_KEY = "test0000indexnow0000key000000000"
_FIXTURE_NAMES = ("home", "sitemap", "hub", "city", "month", "records")
_HARNESS = os.path.join(REPO, "scripts", "backend-for-tests.sh")
def load_fixture(name: str):
with open(os.path.join(_HERE, "fixtures", f"{name}.json"), encoding="utf-8") as fh:
return json.load(fh)
def _not_found(detail: str) -> httpx.HTTPStatusError:
req = httpx.Request("GET", "http://backend.invalid/x")
resp = httpx.Response(404, request=req, json={"detail": detail})
return httpx.HTTPStatusError(detail, request=req, response=resp)
# --- unit tier ---------------------------------------------------------------
@pytest.fixture
def client(monkeypatch):
"""TestClient over the SSR app with api_client replaced by the committed fixtures.
Exercises the rendering layer (content.py + format.py) with zero backend."""
from fastapi.testclient import TestClient
import api_client
import app as ssr_app
fx = {n: load_fixture(n) for n in _FIXTURE_NAMES}
monkeypatch.setattr(api_client, "home", lambda: fx["home"])
monkeypatch.setattr(api_client, "sitemap", lambda: fx["sitemap"])
monkeypatch.setattr(api_client, "hub", lambda: fx["hub"])
monkeypatch.setattr(api_client, "indexnow_key", lambda: FAKE_INDEXNOW_KEY)
def _city(slug, *, origin=None):
if slug != SLUG:
raise _not_found("Unknown city.")
return fx["city"]
def _month(slug, month):
if slug != SLUG or month != MONTH:
raise _not_found("Unknown month.")
return fx["month"]
def _records(slug, *, origin=None):
if slug != SLUG:
raise _not_found("Unknown city.")
return fx["records"]
monkeypatch.setattr(api_client, "city", _city)
monkeypatch.setattr(api_client, "city_month", _month)
monkeypatch.setattr(api_client, "city_records", _records)
return TestClient(ssr_app.app)
# --- integration tier --------------------------------------------------------
def _docker_ok() -> bool:
try:
return subprocess.run(["docker", "info"], capture_output=True, timeout=15).returncode == 0
except Exception:
return False
@pytest.fixture(scope="session")
def backend_service():
"""Pull + run a real backend container; yield its base URL. Skips if Docker isn't
available or the image can't be pulled/started, so the unit tier still runs anywhere."""
if not _docker_ok():
pytest.skip("Docker not available — skipping integration tier")
try:
url = subprocess.run([_HARNESS, "up"], capture_output=True, text=True, timeout=300, check=True).stdout.strip()
except subprocess.CalledProcessError as e:
pytest.skip(f"backend harness failed to start (image unavailable?): {e.stderr[-300:]}")
try:
yield url
finally:
subprocess.run([_HARNESS, "down"], capture_output=True, timeout=60)
@pytest.fixture
def live_client(backend_service, monkeypatch):
"""TestClient over the SSR app with api_client making REAL HTTP to the live backend
(which runs THERMOGRAPH_BASE=/, so the API is at /api/v{N}, no prefix)."""
from fastapi.testclient import TestClient
import api_client
import app as ssr_app
monkeypatch.setattr(api_client, "API_BASE", backend_service)
monkeypatch.setattr(api_client, "_BASE_PREFIX", "")
api_client._cache.clear()
return TestClient(ssr_app.app)