thermograph/tests/conftest.py

148 lines
6.3 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")
# BACKEND first: content_payloads.py's own dependency chain (data/cities.py,
# data/store.py, ...) does a bare `import paths` that must resolve to
# backend/paths.py while THAT chain loads below.
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
# frontend_ssr/ has its OWN paths.py and app.py -- genuinely different modules
# that happen to share backend's bare top-level names. data/cities.py etc.
# above already bound their own `paths` name to backend's module object
# (permanent, unaffected by anything below); swap sys.modules["paths"] to
# frontend_ssr's version just for the span of importing content.py (which
# cascades into content_loader.py, the other consumer), then restore it so any
# LATER lazy backend import (api.homepage's own `import paths`, first
# triggered whenever a test actually calls content_payloads.home_payload())
# still sees backend's version, not frontend's.
_backend_paths_module = sys.modules.get("paths")
_frontend_paths_spec = importlib.util.spec_from_file_location("paths", os.path.join(FRONTEND_SSR, "paths.py"))
_frontend_paths_module = importlib.util.module_from_spec(_frontend_paths_spec)
sys.modules["paths"] = _frontend_paths_module
_frontend_paths_spec.loader.exec_module(_frontend_paths_module)
# Reprioritize sys.path so "app" -- imported lazily, per-test, by the `client`
# fixture below -- resolves to frontend_ssr/app.py rather than backend's
# app.py (the `app:app` launch shim). Nothing triggers a bare `import app`
# before that point, so ordering (not a swap) is enough for this one name.
sys.path.insert(0, FRONTEND_SSR)
import api_client # noqa: E402
import content # noqa: E402
if _backend_paths_module is not None:
sys.modules["paths"] = _backend_paths_module
else:
del sys.modules["paths"]
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)