thermograph/backend/tests/api/test_content_routes.py
Emi Griffith 9d41236ec5
All checks were successful
PR build (required check) / changes (pull_request) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 6s
shell-lint / shellcheck (pull_request) Successful in 10s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 52s
PR build (required check) / gate (pull_request) Successful in 2s
Check derived store before loading history on content routes
The three SEO content handlers (city / month / records) resolved the city by loading its full ~45-year archive up front, only then checking the derived-store cache, so even a cache HIT paid the full-history load (2-6s on prod).

Restructure so the cheap path runs first: city lookup + grid snap + the cheap content_token (indexed MAX(date), no history load) produce the cache key/ETag, and the expensive load (history, recent forecast) moves into the build() closure that _cached only calls on a miss. A store hit, or a 304, now returns without ever touching the archive. Cache keys/kinds/tokens are unchanged.

Add route tests asserting a second identical request loads history zero more times, that 304 revalidation loads nothing, plus payload/404 coverage.
2026-07-24 12:22:52 -07:00

130 lines
5.1 KiB
Python

"""Route tests for the SSR content JSON API (api/content_routes.py).
The point of these is the latency fix: a derived-store cache HIT must be O(1) —
it must NOT load the ~45-year archive. So the weather layer is faked with call
counters, and the core assertion is that a second identical request loads the
history zero more times than the first. ETag/304 revalidation and the 404/payload
shapes are covered alongside, against a fresh per-test store.
"""
import pytest
from fastapi.testclient import TestClient
from web import app as appmod
from data import climate
from data import cities
BASE = "/thermograph/api/v2/content"
CITY = {
"slug": "testville",
"name": "Testville",
"admin1": "Washington",
"country": "United States",
"country_code": "US",
"lat": 47.6062,
"lon": -122.3321,
"population": 100000,
}
@pytest.fixture
def counts():
return {"load_cached_history": 0, "get_history": 0}
@pytest.fixture
def client(monkeypatch, history, recent, counts, tmp_store):
"""TestClient with the weather + city layer faked and the history loaders
wrapped in counters. tmp_store gives each test a fresh derived store so the
miss->hit sequence is deterministic."""
def load_cached_history(cell):
counts["load_cached_history"] += 1
return history.clone()
def get_history(cell):
counts["get_history"] += 1
return history.clone(), {"cached": True, "cache_age_days": 3}
monkeypatch.setattr(climate, "load_cached_history", load_cached_history)
monkeypatch.setattr(climate, "get_history", get_history)
monkeypatch.setattr(climate, "get_recent_forecast", lambda cell: recent.clone())
# Stable token: content_token() -> payloads -> climate.history_max_date.
monkeypatch.setattr(climate, "history_max_date", lambda cell_id: "2026-07-01")
monkeypatch.setattr(climate, "reverse_geocode", lambda lat, lon: "Testville, Washington")
# Only "testville" is a known city; anything else -> 404.
monkeypatch.setattr(cities, "get", lambda slug: CITY if slug == "testville" else None)
return TestClient(appmod.app)
# --- the regression guard: a cache hit loads no history ----------------------
def test_cache_hit_does_not_load_history(client, counts):
"""First request misses -> builds -> loads history exactly once. A second
identical request is served from the derived store and loads history ZERO
more times. This is the whole point of the fix."""
r1 = client.get(f"{BASE}/city/testville")
assert r1.status_code == 200
assert counts["load_cached_history"] == 1
assert counts["get_history"] == 0 # cached load was non-empty, no live fetch
r2 = client.get(f"{BASE}/city/testville")
assert r2.status_code == 200
assert r2.content == r1.content # replayed from the store
assert counts["load_cached_history"] == 1 # unchanged: no history load on the hit
assert counts["get_history"] == 0
def test_304_revalidation_loads_no_history(client, counts):
"""If-None-Match on a stored payload -> 304 with no history load."""
r1 = client.get(f"{BASE}/city/testville")
assert r1.status_code == 200
etag = r1.headers["etag"]
before = counts["load_cached_history"]
r304 = client.get(f"{BASE}/city/testville", headers={"If-None-Match": etag})
assert r304.status_code == 304
assert r304.headers["etag"] == etag
assert counts["load_cached_history"] == before # 304 loaded nothing
def test_month_and_records_hits_skip_history_load(client, counts):
"""Same O(1)-hit guarantee for the month and records handlers."""
for path in ("month/july", "records"):
counts["load_cached_history"] = 0
assert client.get(f"{BASE}/city/testville/{path}").status_code == 200
assert counts["load_cached_history"] == 1
assert client.get(f"{BASE}/city/testville/{path}").status_code == 200
assert counts["load_cached_history"] == 1 # second call is a store hit
# --- payload shapes ----------------------------------------------------------
def test_city_payload_shape(client):
body = client.get(f"{BASE}/city/testville").json()
assert body["city"]["slug"] == "testville"
assert body["canonical_path"] == "/climate/testville"
assert len(body["months"]) == 12
assert body["today_vs_normal"] is not None # recent forecast folded in
def test_month_payload_shape(client):
body = client.get(f"{BASE}/city/testville/month/july").json()
assert body["month_slug"] == "july"
assert body["canonical_path"] == "/climate/testville/july"
def test_records_payload_shape(client):
body = client.get(f"{BASE}/city/testville/records").json()
assert body["canonical_path"] == "/climate/testville/records"
# --- 404s --------------------------------------------------------------------
def test_unknown_slug_is_404(client, counts):
for path in ("city/nope", "city/nope/month/july", "city/nope/records"):
assert client.get(f"{BASE}/{path}").status_code == 404
assert counts["load_cached_history"] == 0 # never reached the archive
def test_unknown_month_is_404(client):
assert client.get(f"{BASE}/city/testville/month/smarch").status_code == 404