"""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" # --- https origin (jsonld url) ----------------------------------------------- def test_public_host_jsonld_url_is_https(client): """The public site is HTTPS-only behind Caddy, which proxies plain HTTP, so x-forwarded-proto reads "http". A request on the configured public host (THERMOGRAPH_BASE_URL defaults to https://thermograph.org) must still fold an https:// origin into the payload's jsonld url; otherwise the frontend renders an http:// canonical/JSON-LD that 308-redirects.""" import json as _json hdr = {"host": "thermograph.org", "x-forwarded-proto": "http"} for path in ("city/testville", "city/testville/records"): s = _json.dumps(client.get(f"{BASE}/{path}", headers=hdr).json()) assert "https://thermograph.org/thermograph/climate/testville" in s assert "http://thermograph.org" not in s def test_non_public_host_jsonld_keeps_forwarded_scheme(client): hdr = {"host": "192.168.1.10:8000", "x-forwarded-proto": "http"} body = client.get(f"{BASE}/city/testville", headers=hdr).json() assert body["jsonld"]["@graph"][0]["url"].startswith("http://192.168.1.10:8000/") # --- 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