148 lines
6.3 KiB
Python
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)
|