2026-07-23 00:40:18 +00:00
|
|
|
"""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.
|
2026-07-21 17:48:55 +00:00
|
|
|
"""
|
2026-07-23 00:40:18 +00:00
|
|
|
import json
|
2026-07-21 17:48:55 +00:00
|
|
|
import os
|
2026-07-23 00:40:18 +00:00
|
|
|
import subprocess
|
2026-07-21 17:48:55 +00:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
2026-07-23 00:40:18 +00:00
|
|
|
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
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
# 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.
|
2026-07-21 17:48:55 +00:00
|
|
|
os.environ.setdefault("THERMOGRAPH_API_BASE_INTERNAL", "http://backend.invalid")
|
|
|
|
|
os.environ.setdefault("THERMOGRAPH_BASE", "/thermograph")
|
|
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
import content # noqa: E402 — imports api_client under the env set above
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
B = content.BASE # "/thermograph"
|
|
|
|
|
SLUG = "london-england-gb" # captured into tests/fixtures/ (GB -> Celsius)
|
|
|
|
|
MONTH = "july"
|
2026-07-21 17:48:55 +00:00
|
|
|
FAKE_INDEXNOW_KEY = "test0000indexnow0000key000000000"
|
2026-07-23 00:40:18 +00:00
|
|
|
_FIXTURE_NAMES = ("home", "sitemap", "hub", "city", "month", "records")
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
_HARNESS = os.path.join(REPO, "scripts", "backend-for-tests.sh")
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
def load_fixture(name: str):
|
|
|
|
|
with open(os.path.join(_HERE, "fixtures", f"{name}.json"), encoding="utf-8") as fh:
|
|
|
|
|
return json.load(fh)
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
# --- unit tier ---------------------------------------------------------------
|
2026-07-21 17:48:55 +00:00
|
|
|
@pytest.fixture
|
2026-07-23 00:40:18 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-07-21 17:48:55 +00:00
|
|
|
def _city(slug, *, origin=None):
|
2026-07-23 00:40:18 +00:00
|
|
|
if slug != SLUG:
|
2026-07-21 17:48:55 +00:00
|
|
|
raise _not_found("Unknown city.")
|
2026-07-23 00:40:18 +00:00
|
|
|
return fx["city"]
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
def _month(slug, month):
|
|
|
|
|
if slug != SLUG or month != MONTH:
|
2026-07-21 17:48:55 +00:00
|
|
|
raise _not_found("Unknown month.")
|
2026-07-23 00:40:18 +00:00
|
|
|
return fx["month"]
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
def _records(slug, *, origin=None):
|
|
|
|
|
if slug != SLUG:
|
2026-07-21 17:48:55 +00:00
|
|
|
raise _not_found("Unknown city.")
|
2026-07-23 00:40:18 +00:00
|
|
|
return fx["records"]
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
|
|
|
monkeypatch.setattr(api_client, "city", _city)
|
2026-07-23 00:40:18 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-07-21 17:48:55 +00:00
|
|
|
|
2026-07-23 00:40:18 +00:00
|
|
|
@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)."""
|
2026-07-21 17:48:55 +00:00
|
|
|
from fastapi.testclient import TestClient
|
2026-07-23 00:40:18 +00:00
|
|
|
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)
|