thermograph/frontend/tests/integration/test_contract.py
2026-07-22 22:23:50 -07:00

72 lines
3.2 KiB
Python

"""Integration tier: run the SSR app against a REAL backend container (pulled by
scripts/backend-for-tests.sh). Exercises the actual HTTP contract instead of backend
source — catches drift the unit fixtures can't. Asserts the contract (reachability,
version negotiation, payload shape, SSR renders 200), not exact climate numbers.
Skips automatically when Docker/the image isn't available (see tests/conftest.py).
"""
import json
import os
import httpx
import pytest
B = "/thermograph"
SLUG = "london-england-gb"
MONTH = "july"
API = "v2" # frontend api_client.API_VERSION
_FIXTURES = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "fixtures")
def _live(backend_service, path):
# The backend container runs THERMOGRAPH_BASE=/, so its API is at /api/<ver>/...
return httpx.get(f"{backend_service}{path}", timeout=40.0)
def test_backend_version_is_compatible(backend_service):
"""The live backend advertises a contract the frontend (API_VERSION=v2) can talk to."""
v = _live(backend_service, "/api/version").json()
assert v["backend_version"] == "2", v # frontend targets /api/v2
assert int(v["min_frontend"]) <= 2, v # backend still accepts this frontend
@pytest.mark.parametrize("ep", ["home", "sitemap", "hub", "city", "month", "records"])
def test_fixture_shapes_match_live_backend(backend_service, ep):
"""The committed unit fixtures still match the live payload shapes — proves the two
tiers can't silently diverge. Compares top-level structure, not values."""
paths = {
"home": f"/api/{API}/content/home",
"sitemap": f"/api/{API}/content/sitemap",
"hub": f"/api/{API}/content/hub",
"city": f"/api/{API}/content/city/{SLUG}",
"month": f"/api/{API}/content/city/{SLUG}/month/{MONTH}",
"records": f"/api/{API}/content/city/{SLUG}/records",
}
live = _live(backend_service, paths[ep])
assert live.status_code == 200, live.text[:200]
live_json = live.json()
with open(os.path.join(_FIXTURES, f"{ep}.json"), encoding="utf-8") as fh:
fixture = json.load(fh)
if isinstance(fixture, dict):
assert set(live_json.keys()) == set(fixture.keys()), (
f"{ep}: live keys {sorted(live_json)} != fixture keys {sorted(fixture)} "
"— run scripts/capture-fixtures.sh")
else:
assert type(live_json) is type(fixture)
def test_unknown_city_404(backend_service):
assert _live(backend_service, f"/api/{API}/content/city/nope-not-a-city").status_code == 404
@pytest.mark.parametrize("path", ["/", "/climate", f"/climate/{SLUG}", f"/climate/{SLUG}/{MONTH}",
f"/climate/{SLUG}/records", "/sitemap.xml", "/robots.txt"])
def test_ssr_renders_against_live_backend(live_client, path):
"""The SSR app rendering each page end-to-end against the live backend proves every
field content.py reads is actually present in the real payload."""
r = live_client.get(f"{B}{path}")
assert r.status_code == 200, f"{path} -> {r.status_code}"
assert "__ORIGIN__" not in r.text
def test_ssr_unknown_city_404(live_client):
assert live_client.get(f"{B}/climate/nope-not-a-city").status_code == 404