thermograph/tests/test_content.py
Emi Griffith a705c154cd SEO: hand-curated notable weather event per city (falls back to blurb) (#100)
Auto-sourcing a notable weather event from Wikipedia search proved unreliable
(wrong matches, low coverage), so city_events.py is a small, fact-checked list of
one iconic event per city (Katrina/New Orleans, Harvey/Houston, the 1952 Great
Smog/London, the 2021 heat dome/Seattle, ...) — 31 cities, each with a Wikipedia
link. The city page shows a 'Notable weather in {city}' callout when one exists and
otherwise just the flavor blurb. Tests check the curated slugs are all valid and the
callout renders (and is absent for uncurated cities).
2026-07-16 01:23:45 +00:00

108 lines
3.9 KiB
Python

"""Tests for the crawlable SEO content pages: the city set, robots/sitemap, and
that the server-rendered pages contain the stats in the HTML (with the weather
layer faked, like test_api.py)."""
import pytest
from fastapi.testclient import TestClient
import app as appmod
import cities
import climate
# BASE defaults to /thermograph in tests (THERMOGRAPH_BASE unset).
B = "/thermograph"
SLUG = "london-england-gb" # present in cities.json
@pytest.fixture
def client(monkeypatch, history, recent):
monkeypatch.setattr(climate, "load_cached_history", lambda cell: history.clone())
monkeypatch.setattr(climate, "get_recent_forecast", lambda cell: recent.clone())
monkeypatch.setattr(climate, "get_history", lambda cell: (history.clone(), {"cached": True}))
return TestClient(appmod.app)
def test_city_set_slugs_unique_and_lookup():
slugs = cities.all_slugs()
assert len(slugs) == len(set(slugs)) >= 100
assert cities.get(SLUG) is not None
assert cities.get("does-not-exist") is None
def test_robots_txt(client):
r = client.get(f"{B}/robots.txt")
assert r.status_code == 200
assert "Sitemap:" in r.text and "/sitemap.xml" in r.text
assert "Disallow: /thermograph/api/" in r.text
def test_sitemap_lists_city_urls(client):
r = client.get(f"{B}/sitemap.xml")
assert r.status_code == 200
assert "<urlset" in r.text
assert f"/climate/{SLUG}</loc>" in r.text
assert f"/climate/{SLUG}/july</loc>" in r.text
assert f"/climate/{SLUG}/records</loc>" in r.text
def test_city_page_renders_stats_in_html(client):
r = client.get(f"{B}/climate/{SLUG}")
assert r.status_code == 200
b = r.text
assert "London" in b and "climate" in b.lower()
assert 'rel="canonical"' in b and f"/climate/{SLUG}" in b
assert '"@type":"Dataset"' in b
assert "average temperatures by month" in b.lower()
# a real number rendered (°F appears in the normals/today text)
assert "°F" in b
def test_city_404(client):
assert client.get(f"{B}/climate/nope-not-a-city").status_code == 404
def test_curated_events_have_valid_slugs():
import city_events
slugs = set(cities.all_slugs())
assert len(city_events.EVENTS) >= 20
assert all(s in slugs for s in city_events.EVENTS), \
[s for s in city_events.EVENTS if s not in slugs]
def test_curated_event_renders(client):
b = client.get(f"{B}/climate/new-orleans-louisiana-us").text
assert "Notable weather in New Orleans" in b
assert "Hurricane Katrina" in b
def test_uncurated_city_has_no_event_section(client):
b = client.get(f"{B}/climate/shanghai-cn").text
assert "city-event" not in b # falls back to the flavor blurb
def test_city_travel_cta_prefills_compare(client):
b = client.get(f"{B}/climate/{SLUG}").text
assert "Thinking of visiting" in b
assert "/compare#loc=" in b # the city is pre-loaded on the compare page
def test_city_blurb_renders_with_attribution(client, monkeypatch):
monkeypatch.setattr(cities, "flavor",
lambda slug: {"extract": "Testville is a lovely place to test.",
"url": "https://en.wikipedia.org/wiki/Testville", "title": "Testville"})
b = client.get(f"{B}/climate/{SLUG}").text
assert "Testville is a lovely place to test." in b
assert "via Wikipedia" in b # CC BY-SA attribution link
def test_month_and_records(client):
assert client.get(f"{B}/climate/{SLUG}/july").status_code == 200
assert client.get(f"{B}/climate/{SLUG}/records").status_code == 200
assert client.get(f"{B}/climate/{SLUG}/notamonth").status_code == 404
def test_hub_glossary_about(client):
assert client.get(f"{B}/climate").status_code == 200
assert client.get(f"{B}/glossary").status_code == 200
assert client.get(f"{B}/glossary/percentile").status_code == 200
assert client.get(f"{B}/glossary/not-a-term").status_code == 404
assert client.get(f"{B}/about").status_code == 200