thermograph/tests/integration/test_contract.py

73 lines
3.2 KiB
Python
Raw Normal View History

"""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