121 lines
4.7 KiB
Python
121 lines
4.7 KiB
Python
|
|
"""Shared test setup for the SSR service.
|
||
|
|
|
||
|
|
Stage-3-transitional: this package still lives in the same repo as the
|
||
|
|
backend, so a `client` fixture can fake api_client by calling the REAL
|
||
|
|
backend payload builders (api.content_payloads) against the same synthetic
|
||
|
|
history/recent fixtures backend/tests/conftest.py uses, instead of hand-
|
||
|
|
maintaining a parallel set of literal JSON fixtures. This means a shape drift
|
||
|
|
between content_payloads.py and how content.py consumes it fails here, not
|
||
|
|
just in backend/tests/web/test_content.py -- and the two suites assert on
|
||
|
|
identical numbers, so they can never quietly diverge.
|
||
|
|
"""
|
||
|
|
import importlib.util
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||
|
|
FRONTEND_SSR = os.path.dirname(_HERE)
|
||
|
|
REPO_ROOT = os.path.dirname(FRONTEND_SSR)
|
||
|
|
BACKEND = os.path.join(REPO_ROOT, "backend")
|
||
|
|
|
||
|
|
sys.path.insert(0, FRONTEND_SSR)
|
||
|
|
sys.path.insert(0, BACKEND)
|
||
|
|
|
||
|
|
os.environ.setdefault("THERMOGRAPH_API_BASE_INTERNAL", "http://backend.invalid")
|
||
|
|
os.environ.setdefault("THERMOGRAPH_BASE", "/thermograph")
|
||
|
|
|
||
|
|
|
||
|
|
def _load_module(name: str, path: str):
|
||
|
|
"""importlib rather than a bare `import conftest` -- pytest registers
|
||
|
|
THIS file's own module under the plain name "conftest" too, so a bare
|
||
|
|
import here could self-collide instead of resolving to backend's file."""
|
||
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
||
|
|
mod = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(mod)
|
||
|
|
return mod
|
||
|
|
|
||
|
|
|
||
|
|
# Reuse the exact synthetic weather fixtures backend/tests/conftest.py
|
||
|
|
# defines (its module-level code also sandboxes data/store/audit paths into a
|
||
|
|
# tmp dir), so a page rendered here and the same page rendered by the
|
||
|
|
# backend's own test_content.py agree on every number.
|
||
|
|
_backend_conftest = _load_module("_backend_test_conftest", os.path.join(BACKEND, "tests", "conftest.py"))
|
||
|
|
make_history = _backend_conftest.make_history
|
||
|
|
make_recent = _backend_conftest.make_recent
|
||
|
|
|
||
|
|
from api import content_payloads # noqa: E402
|
||
|
|
from api import sitemap as sitemap_mod # noqa: E402
|
||
|
|
from data import cities # noqa: E402
|
||
|
|
|
||
|
|
import api_client # noqa: E402
|
||
|
|
import content # noqa: E402
|
||
|
|
|
||
|
|
B = content.BASE
|
||
|
|
SLUG = "london-england-gb" # present in cities.json; GB -> renders in Celsius
|
||
|
|
FAKE_INDEXNOW_KEY = "test0000indexnow0000key000000000"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(scope="session")
|
||
|
|
def history():
|
||
|
|
return make_history()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(scope="session")
|
||
|
|
def recent(history):
|
||
|
|
return make_recent(history)
|
||
|
|
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def client(monkeypatch, history, recent):
|
||
|
|
"""A TestClient over the SSR app, with api_client's HTTP calls replaced by
|
||
|
|
direct calls into the real backend payload builders -- see module
|
||
|
|
docstring. Origin defaults to the TestClient's own base URL, matching what
|
||
|
|
a real request through content.py's _origin(request) would forward."""
|
||
|
|
def _city(slug, *, origin=None):
|
||
|
|
c = cities.get(slug)
|
||
|
|
if c is None:
|
||
|
|
raise _not_found("Unknown city.")
|
||
|
|
return content_payloads.city_payload(origin or "http://testserver", B, c, history, recent)
|
||
|
|
|
||
|
|
def _city_month(slug, month):
|
||
|
|
c = cities.get(slug)
|
||
|
|
if c is None:
|
||
|
|
raise _not_found("Unknown city.")
|
||
|
|
if month not in content_payloads.MONTH_INDEX:
|
||
|
|
raise _not_found("Unknown month.")
|
||
|
|
return content_payloads.month_payload(B, c, history, content_payloads.MONTH_INDEX[month])
|
||
|
|
|
||
|
|
def _city_records(slug, *, origin=None):
|
||
|
|
c = cities.get(slug)
|
||
|
|
if c is None:
|
||
|
|
raise _not_found("Unknown city.")
|
||
|
|
return content_payloads.records_payload(origin or "http://testserver", B, c, history)
|
||
|
|
|
||
|
|
def _sitemap():
|
||
|
|
return [{"path": p, "changefreq": cf, "priority": pr} for p, cf, pr in sitemap_mod.sitemap_entries()]
|
||
|
|
|
||
|
|
monkeypatch.setattr(api_client, "city", _city)
|
||
|
|
monkeypatch.setattr(api_client, "city_month", _city_month)
|
||
|
|
monkeypatch.setattr(api_client, "city_records", _city_records)
|
||
|
|
monkeypatch.setattr(api_client, "hub", content_payloads.hub_payload)
|
||
|
|
monkeypatch.setattr(api_client, "home", content_payloads.home_payload)
|
||
|
|
monkeypatch.setattr(api_client, "sitemap", _sitemap)
|
||
|
|
monkeypatch.setattr(api_client, "indexnow_key", lambda: FAKE_INDEXNOW_KEY)
|
||
|
|
# api_client's TTL cache is keyed by path/origin, not by test -- stale
|
||
|
|
# entries from an earlier test would otherwise leak real-looking but
|
||
|
|
# wrong data into this one.
|
||
|
|
api_client._cache.clear()
|
||
|
|
|
||
|
|
import app as appmod
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
return TestClient(appmod.app)
|