"""Analytics endpoints (API 1.1) on the synthetic fixture (8 countries × 14 indicators, 1990–2024).""" from __future__ import annotations import pytest from tests.api.conftest import assert_meta, assert_provenance, value def test_pulse(get): body = get("/pulse").json() assert_meta(body) assert body["year_reference"] == 2024 # fixture: 8 countries only, below the 30-country pool threshold → no items but a valid envelope assert body["items"] == [] and body["summary"]["n_countries_reporting"] == 0 assert "n_record_highs" in body["summary"] def test_movers_window_1_and_kinds(get): body = get("/movers?window=1&limit=10").json() assert body["window"] == 1 and body["categories"][0] == "all" and "structural" in body["kinds"] assert body["n"] == len(body["items"]) > 0 it = body["items"][0] for k in ("country", "indicator", "kind", "value", "delta", "direction", "interpretation", "headline", "provenance"): assert k in it assert it["direction"] in ("up", "down") assert_provenance(it["provenance"]) rec = get("/movers?window=1&kind=record&limit=50").json() assert rec["items"] and all(i["kind"] in ("record_high", "record_low") for i in rec["items"]) imp = get("/movers?window=1&kind=improvement&limit=50").json() assert all(i["interpretation"] == "improvement" for i in imp["items"]) econ = get("/movers?window=1&category=economic&limit=50").json() assert all(i["indicator"]["topic"] in ("economy", "government", "trade", "income") for i in econ["items"]) get("/movers?window=3", status=400) get("/movers?category=weird", status=400) def test_movers_multi_year_window_needs_pool(get): body = get("/movers?window=10&limit=10").json() # 8 countries < 20 → empty but valid assert body["window"] == 10 and body["items"] == [] body = get("/movers?window=5&kind=record").json() assert body["items"] == [] def test_extremes_envelope(get): body = get("/extremes?window=10").json() assert body["window"] == "10" and body["filter_note"] == "Countries above 1M inhabitants" assert body["facets"] == [] # every facet needs ≥ 30 countries in the window get("/extremes?window=7", status=400) def test_scatter(get): body = get("/scatter?x=gdp-per-capita&y=life-expectancy&year=2024&log_x=true").json() assert_meta(body) assert body["year_used"] == 2024 and body["n"] == 8 and body["size"]["id"] == "population" p = next(pt for pt in body["points"] if pt["id"] == "CAN") assert p["x"] == pytest.approx(value("CAN", "gdp-per-capita", 2024)) and p["y"] == pytest.approx(value("CAN", "life-expectancy", 2024)) assert p["size"] == pytest.approx(value("CAN", "population", 2024)) and p["year_x"] == 2024 st = body["stats"] assert st["n"] == 8 and -1 <= st["pearson"] <= 1 and -1 <= st["spearman"] <= 1 and st["ols"]["slope"] is not None assert st["log_x"] is True and st["log_y"] is False assert "causation" in body["note"] none = get("/scatter?x=gdp-per-capita&y=life-expectancy&size=none").json() assert none["size"] is None and none["stats"]["log_x"] is False and none["year_used"] == 2024 # auto: ratio max/min < 50 in the fixture get("/scatter?x=gdp&y=gdp", status=400) get("/scatter?x=gdp&y=nope", status=404) grp = get("/scatter?x=gdp-per-capita&y=life-expectancy&group=g7").json() assert grp["group"]["id"] == "g7" and grp["n"] == 5 def test_trajectory(get): body = get("/trajectory?x=gdp-per-capita&y=life-expectancy&from=2000&to=2005").json() assert body["years"] == [2000, 2001, 2002, 2003, 2004, 2005] assert len(body["countries"]) == 8 and set(body["series"]) == {c["id"] for c in body["countries"]} s = body["series"]["IND"] assert len(s["x"]) == 6 and s["x"][0] == pytest.approx(value("IND", "gdp-per-capita", 2000)) assert s["size"][0] == pytest.approx(value("IND", "population", 2000)) assert body["domains"]["x"][0] <= min(v for v in s["x"]) and body["log_x"] is True # pooled x spans > 50× → log assert body["provenance"] auto = get("/trajectory?x=gdp-per-capita&y=life-expectancy").json() assert auto["years"][0] == 1990 and auto["years"][-1] == 2024 # < 50 countries → every year with data get("/trajectory?x=gdp&y=gdp", status=400) def test_finder(get): body = get("/finder?f=gdp-per-capita:gt:40000&f=population:gt:100000000").json() assert body["mode"] == "and" and [f["indicator"]["id"] for f in body["filters"]] == ["gdp-per-capita", "population"] assert body["filters"][0]["op"] == "gt" and body["filters"][0]["value"] == 40000 and body["filters"][0]["year_used"] == 2024 ids = [i["country"]["id"] for i in body["items"]] assert ids == ["JPN", "USA"] # rich (2024 gpc > 40k) and populous; sorted by gpc desc (JPN 69k > USA 65k in the fixture) assert body["n_matching"] == 2 and body["n_evaluated"] == 8 assert body["items"][0]["matched"] == ["gdp-per-capita", "population"] assert_provenance(body["items"][0]["values"]["gdp-per-capita"]["provenance"]) any_ = get("/finder?f=gdp-per-capita:gt:40000&f=population:gt:1000000000&mode=or").json() assert "IND" in [i["country"]["id"] for i in any_["items"]] # India by population only between = get("/finder?f=life-expectancy:between:60..70&sort=population:asc").json() assert all(60 <= i["values"]["life-expectancy"]["value"] <= 70 for i in between["items"]) reg = get("/finder?f=population:gt:1®ion=g7").json() assert reg["n_matching"] == 5 get("/finder", status=400) get("/finder?f=gdp-per-capita:huge:1", status=400) get("/finder?f=nope:gt:1", status=404) def test_peers(get): body = get("/peers?y=life-expectancy&x=gdp-per-capita&log_x=true").json() assert body["year_used"] == 2024 and body["n"] == 8 and body["method"] == "theil-sen" assert body["fit"]["log_x"] is True and body["fit"]["residual_scale"] is not None pts = {p["id"]: p for p in body["points"]} assert pts["CAN"]["expected"] is not None and pts["CAN"]["residual"] == pytest.approx(pts["CAN"]["y"] - pts["CAN"]["expected"]) assert body["above"][0]["residual_z"] >= body["above"][-1]["residual_z"] and body["below"][0]["residual_z"] <= body["below"][-1]["residual_z"] assert body["pairs"] and "descriptive" in body["note"] ols = get("/peers?y=life-expectancy&x=gdp-per-capita&method=ols").json() assert ols["fit"]["r2"] is not None get("/peers?y=gdp&x=gdp", status=400) def test_related_distribution_frames(get): rel = get("/indicators/life-expectancy/related?min_n=5").json() assert rel["indicator"]["id"] == "life-expectancy" and rel["items"] and "causation" in rel["note"] assert all(i["indicator"]["id"] != "life-expectancy" for i in rel["items"]) assert abs(rel["items"][0]["spearman"]) >= abs(rel["items"][-1]["spearman"]) assert get("/indicators/life-expectancy/related").json()["items"] == [] # min_n 40 > 8 countries d = get("/indicators/life-expectancy/distribution?highlight=JPN").json() assert d["year_used"] == 2024 and d["n"] == 8 and sum(d["histogram"]["counts"]) == 8 and len(d["histogram"]["edges"]) == 21 assert d["stats"]["min"] <= d["stats"]["median"] <= d["stats"]["max"] assert d["highlight"]["country"]["id"] == "JPN" and d["highlight"]["rank"] == 1 and d["highlight"]["percentile"] == pytest.approx(100.0) assert d["by_income"] and d["by_region"] assert_provenance(d["provenance"]) lg = get("/indicators/gdp-per-capita/distribution?bins=5").json() assert lg["log"] is False and len(lg["histogram"]["counts"]) == 5 # auto-log needs a > 50× spread; the fixture's 2024 spread is ~25× fr = get("/indicators/gdp-per-capita/frames?from=2000&to=2003").json() assert fr["years"] == [] or fr["years"][0] >= 2000 # fixture has 8 countries (< 20 per year) → no frames assert fr["legend"]["n_classes"] >= 1 and isinstance(fr["values"], dict) def test_quality_and_updates(get): q = get("/indicators/life-expectancy/quality").json() assert q["n_countries"] == 8 and q["n_countries_total"] == 8 and q["coverage_pct"] == 100.0 assert q["first_year"] == 1990 and q["last_year"] == 2024 and q["years_with_50plus"] == 0 assert "fresh" in q["badges"] and "historical" not in q["badges"] and q["sources"] get("/indicators/nope/quality", status=404) cq = get("/countries/canada/quality").json() assert cq["country"]["id"] == "CAN" and cq["summary"]["n_with_data"] == 14 and cq["summary"]["latest_year"] == 2024 it = next(i for i in cq["items"] if i["indicator"]["id"] == "government-debt-pct-gdp") assert it["first_year"] == 1995 and it["n_points"] == 30 and it["missing_years"] == 0 and it["continuity_pct"] == 100.0 assert "fresh" in it["badges"] u = get("/updates").json() assert_meta(u) assert u["snapshot"]["observations"] > 0 and u["snapshot"]["values_changed"] == 0 ids = {s["source"]["id"]: s for s in u["sources"]} assert ids["worldbank"]["status"] == "ok" and ids["who"]["status"] == "failed" and ids["owid"]["status"] == "partial" assert ids["worldbank"]["n_observations"] > 0 and ids["worldbank"]["latest_year"] == 2024 assert len(u["recent_runs"]) == 6 and all("raw_path" not in r for r in u["recent_runs"]) assert u["indicators_recently_updated"] def test_race_and_regions_compare(get): r = get("/rankings/gdp/race?top=3&from=2020").json() assert r["years"] == [] or r["frames"][0]["year"] >= 2020 # fixture rankings have 8 countries (< 20) → no frames assert r["top"] == 3 and r["indicator"]["id"] == "gdp" and r["group"]["id"] == "world" get("/rankings/nope/race", status=404) rc = get("/regions/compare?a=g7&b=brics").json() assert [g["id"] for g in rc["groups"]] == ["g7", "brics"] row = next(x for x in rc["rows"] if x["indicator"]["id"] == "population") assert row["kind"] == "sum" and row["values"]["g7"]["n"] == 5 and row["values"]["brics"]["n"] == 2 assert rc["shares"]["g7"]["population_share_pct"] + rc["shares"]["brics"]["population_share_pct"] < 100.01 h = rc["history"]["gdp-per-capita"] assert h["kind"] == "weighted_mean" and h["years"][0] == 1990 and len(h["g7"]) == len(h["years"]) get("/regions/compare?a=nope&b=g7", status=404) def test_story_and_dna_reference(get): s = get("/countries/canada/story").json() assert s["country"]["id"] == "CAN" and s["since"] == 1990 and s["items"] pop = next(i for i in s["items"] if i["indicator"]["id"] == "population") assert pop["first"]["year"] == 1990 and pop["last"]["year"] == 2024 and pop["change_pct"] > 0 and pop["cagr"] == pytest.approx(1.0, abs=0.05) assert pop["rank_first"]["year"] == 1990 and pop["rank_last"]["rank"] >= 1 and len(pop["series"]) == 35 assert pop["text"].startswith("Canada: Population") and "1990" in pop["text"] and "2024" in pop["text"] assert_provenance(pop["provenance"]) le = next(i for i in s["items"] if i["indicator"]["id"] == "life-expectancy") assert le["cagr"] is None and "yrs" in le["text"] d = get("/countries/canada/dna?reference=world").json() assert d["reference"]["kind"] == "world" and set(d["reference"]["dims"].values()) == {50.0} d = get("/countries/canada/dna?reference=income").json() assert d["reference"]["kind"] == "income" and d["reference"]["id"] == "hic" and d["reference"]["dims"]["income"] is not None d = get("/countries/canada/dna?reference=fra").json() assert d["reference"]["kind"] == "country" and d["reference"]["id"] == "FRA" assert get("/countries/canada/dna").json()["reference"] is None get("/countries/canada/dna?reference=xxx", status=404) def test_search_intents(get): hits = get("/search?q=compare%20canada%20usa").json()["hits"] assert hits[0]["type"] == "action" and hits[0]["action"] == "compare" and hits[0]["url"] == "/compare/canada/united-states" hits = get("/search?q=canada%20vs%20france%20vs%20japan").json()["hits"] assert hits[0]["url"] == "/compare/canada/france/japan" hits = get("/search?q=rank%20gdp").json()["hits"] assert hits[0]["action"] == "ranking" and hits[0]["url"] == "/rankings/gdp" hits = get("/search?q=inflation%20g7").json()["hits"] assert hits[0]["action"] == "group_ranking" and hits[0]["url"] == "/rankings/inflation?group=g7" hits = get("/search?q=life%20expectancy%20map").json()["hits"] assert hits[0]["action"] == "explore" and hits[0]["url"] == "/explore?indicator=life-expectancy" # plain queries are unchanged assert get("/search?q=canada").json()["hits"][0]["type"] == "country" assert all(h["type"] != "action" for h in get("/search?q=gdp&type=indicator").json()["hits"])