from __future__ import annotations import csv import io import pytest from tests.api.conftest import COUNTRIES, assert_meta, assert_provenance, value def test_list_countries(get): body = get("/countries").json() assert_meta(body) assert body["n"] == len(COUNTRIES) ids = [c["id"] for c in body["items"]] assert ids == sorted(ids, key=lambda i: next(c["name"] for c in body["items"] if c["id"] == i)) can = next(c for c in body["items"] if c["id"] == "CAN") assert can["slug"] == "canada" and can["flag"] and can["region_name"] == "North America" and can["income"] == "HIC" assert can["population_latest"] == pytest.approx(value("CAN", "population", 2024)) assert can["gdp_per_capita_latest"] == pytest.approx(value("CAN", "gdp-per-capita", 2024)) assert can["coverage_pct"] == 100.0 def test_list_countries_filters_and_sort(get): g7 = get("/countries?region=g7&sort=gdp").json() assert {c["id"] for c in g7["items"]} == {"CAN", "USA", "FRA", "DEU", "JPN"} vals = [c["gdp_latest"] for c in g7["items"]] assert vals == sorted(vals, reverse=True) assert get("/countries?region=north-america").json()["n"] == 2 assert {c["id"] for c in get("/countries?income=LMC").json()["items"]} == {"IND", "NGA"} assert get("/countries?q=jap").json()["items"][0]["id"] == "JPN" assert get("/countries?region=nowhere", status=404) assert get("/countries?sort=bogus", status=422) def test_country_overview(get): body = get("/countries/canada").json() assert_meta(body) c = body["country"] assert c["id"] == "CAN" and c["iso2"] == "CA" and c["capital"] == "Ottawa" and c["borders"] == ["USA"] assert "g7" in {g["id"] for g in body["groups"]} and "world" in {g["id"] for g in body["groups"]} assert body["coverage"]["n_indicators"] == 14 assert body["freshness"]["source_updated_at"].startswith("2026-07-01") and body["freshness"]["built_at"] from countryatlas.registry import topics as registry_topics headline = {m["indicator"]: m for m in body["headline"]} assert list(headline) == registry_topics()["headline"] # read from topics.yaml at request time (cache cleared per snapshot) assert list(headline)[:3] == ["population", "gdp", "gdp-per-capita"] # indicators in the headline but absent from the snapshot still render as "no data" absent = [m for m in body["headline"] if m["indicator"] not in ("population", "gdp", "gdp-per-capita", "gdp-growth", "inflation", "unemployment-rate", "life-expectancy", "median-age", "co2-per-capita", "renewable-electricity-share", "internet-users", "government-debt-pct-gdp")] assert all(m["has_data"] is False and m["formatted"] == "—" and m["provenance"] is None for m in absent) gpc = headline["gdp-per-capita"] assert gpc["has_data"] and gpc["year"] == 2024 and gpc["value"] == pytest.approx(value("CAN", "gdp-per-capita", 2024)) assert gpc["change"]["pct"] == pytest.approx(3.0) and gpc["change"]["formatted"] == "+3.0 %" assert gpc["rank_world"] and gpc["n_world"] == 8 and gpc["rank_region"] and gpc["n_region"] == 2 assert gpc["formatted"].startswith("US$") and gpc["formatted"].endswith("k") and gpc["formatted_short"] == gpc["formatted"][3:] assert headline["inflation"]["formatted"].endswith(" %") and headline["life-expectancy"]["formatted"].endswith(" yrs") assert len(gpc["sparkline"]) == 30 and gpc["sparkline"][-1][0] == 2024 and isinstance(gpc["sparkline"][-1][0], int) assert_provenance(gpc["provenance"]) assert gpc["provenance"]["url"] == "https://data.worldbank.org/indicator/NY.GDP.PCAP.CD?locations=CA" assert gpc["provenance"]["series_code"] == "NY.GDP.PCAP.CD" and gpc["provenance"]["licence"] == "CC BY 4.0" # OWID provenance for co2 assert headline["co2-per-capita"]["provenance"]["url"] == "https://github.com/owid/co2-data" assert headline["median-age"]["provenance"]["url"] == "https://ourworldindata.org/grapher/median-age" topics = {t["id"]: t for t in body["topics"]} assert topics["economy"]["n_with_data"] >= 5 and topics["income"]["n_with_data"] == 0 assert [t["order"] for t in body["topics"]] == sorted(t["order"] for t in body["topics"]) assert body["neighbours"][0]["id"] == "USA" def test_country_case_insensitive_and_iso2(get): assert get("/countries/can").json()["country"]["id"] == "CAN" assert get("/countries/CANADA").json()["country"]["id"] == "CAN" assert get("/countries/ca").json()["country"]["id"] == "CAN" def test_country_404_problem_json(client): r = client.get("/api/v1/countries/atlantis") assert r.status_code == 404 assert r.headers["content-type"].startswith("application/problem+json") body = r.json() assert body["title"] == "Country not found" and body["status"] == 404 and "atlantis" in body["detail"] assert body["instance"] == "/api/v1/countries/atlantis" def test_country_topic(get): body = get("/countries/CAN/topics/economy").json() assert body["topic"]["id"] == "economy" and body["n_indicators"] > body["n_with_data"] > 0 subs = [s["subtopic"] for s in body["subtopics"]] assert subs[0] == "Output" # registry order all_inds = [i for s in body["subtopics"] for i in s["indicators"]] with_data = [i for i in all_inds if i["has_data"]] without = [i for i in all_inds if not i["has_data"]] assert with_data and without assert without[0]["provenance"] is None and without[0]["value"] is None for m in with_data: assert_provenance(m["provenance"]) assert m["sparkline"] get("/countries/CAN/topics/astrology", status=404) def test_country_series(get): body = get("/countries/CAN/series/gdp?from=2010&to=2026&include_alt=true").json() assert_meta(body) assert body["indicator"]["id"] == "gdp" and body["country"]["id"] == "CAN" and body["unit"] == "current US$" years = [v["year"] for v in body["values"]] assert years == list(range(2010, 2027)) fc = [v for v in body["values"] if v["is_forecast"]] assert [v["year"] for v in fc] == [2025, 2026] and fc[0]["source_id"] == "imf" assert body["values"][0]["value"] == pytest.approx(value("CAN", "gdp", 2010)) for v in body["values"]: assert_provenance(v["provenance"]) assert body["provenance"]["source"] == "worldbank" assert {s["source"] for s in body["sources"]} == {"worldbank", "imf"} assert body["alternatives"] and body["alternatives"][0]["source_id"] == "imf" assert body["stats"]["last"]["year"] == 2024 and body["stats"]["cagr"] == pytest.approx(4.03, abs=0.01) no_fc = get("/countries/CAN/series/gdp?include_forecast=false").json() assert all(not v["is_forecast"] for v in no_fc["values"]) get("/countries/CAN/series/not-an-indicator", status=404) get("/countries/CAN/series/gdp?from=2020&to=2010", status=400) def test_country_changes_events(get): ch = get("/countries/BRA/changes").json() assert ch["n"] > 0 sev = [c["severity"] for c in ch["items"]] assert sev == sorted(sev, reverse=True) assert ch["items"][0]["headline"] and ch["items"][0]["indicator"]["slug"] assert_provenance(ch["items"][0]["provenance"]) ev = get("/countries/USA/events?limit=5").json() assert 0 < ev["n"] <= 5 and ev["items"][0]["kind"] in ("sign_flip", "yoy_jump") get("/countries/ZZZ/changes", status=404) def test_country_similar_insights_dna(get): sim = get("/countries/CAN/similar?mode=overall").json() assert sim["mode"] == "overall" and "economic" in sim["modes"] assert sim["peers"][0]["country"]["id"] in ("USA", "DEU", "FRA", "JPN") and sim["peers"][0]["score"] > 50 assert sim["peers"][0]["contributions"] ins = get("/countries/CAN/insights").json() assert ins["items"] and "population grew" in ins["items"][0]["text"] or "Life expectancy" in ins["items"][0]["text"] assert any(i["provenance"] for i in ins["items"]) dna = get("/countries/CAN/dna").json() assert dna["year_ref"] == 2024 and 0 <= dna["dims"]["income"] <= 100 and dna["dims"]["trade"] is None assert any(d["id"] == "income" and d["label"] == "Income" for d in dna["dimensions"]) def test_country_download(client): r = client.get("/api/v1/countries/CAN/download.csv") assert r.status_code == 200 and r.headers["content-type"].startswith("text/csv") assert 'filename="countryatlas-canada.csv"' in r.headers["content-disposition"] lines = r.text.splitlines() assert lines[0].startswith("# CountryAtlas export") rows = list(csv.DictReader(io.StringIO("\n".join(lines[1:])))) assert len(rows) > 400 and {"source", "series_code", "retrieved_at", "url", "licence"} <= set(rows[0]) assert all(r["country_id"] == "CAN" for r in rows) j = client.get("/api/v1/countries/CAN/download.json?include_forecast=false").json() assert j["n"] == sum(1 for r in rows if r["is_forecast"] == "False") and j["rows"][0]["source"] assert client.get("/api/v1/countries/CAN/download.xml").status_code == 400