# ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : api/tests/test_api.py # Purpose : API tests (monthly v2.1) — every router, ETag / CSV / pagination. # ============================================================================= """API test suite (runs against the real canonical monthly parquet).""" from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from fastapi.testclient import TestClient from app.main import app client = TestClient(app) def test_health(): r = client.get("/health") assert r.status_code == 200 assert r.json()["status"] == "ok" def test_meta_credits_author_and_monthly(): r = client.get("/v1/meta") assert r.status_code == 200 body = r.json() assert body["author"] == "Simon-Pierre Boucher" assert body["contact"] == "contact@spboucher.ai" assert body["frequency"] == "monthly" assert body["series_count"] > 100 assert body["periods"] >= 60 def test_index_series_full(): r = client.get("/v1/index", params={"geography": "quebec-city", "type": "condo"}) assert r.status_code == 200 body = r.json() assert body["frequency"] == "monthly" obs = body["observations"] assert len(obs) == body["total_observations"] >= 60 last = obs[-1] for key in ("lower_95", "upper_95", "reliability_grade", "transactions", "monthly_pct", "yoy_pct"): assert key in last # uncertainty is never hidden def test_index_pagination(): r = client.get("/v1/index", params={"geography": "quebec", "type": "all", "limit": 10, "offset": 5}) body = r.json() assert len(body["observations"]) == 10 assert body["offset"] == 5 def test_index_csv_export(): r = client.get("/v1/index", params={"geography": "montreal", "type": "condo", "format": "csv"}) assert r.status_code == 200 assert r.headers["content-type"].startswith("text/csv") assert r.text.splitlines()[0].startswith("period") def test_index_etag_304(): r1 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"}) etag = r1.headers.get("etag") assert etag r2 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"}, headers={"If-None-Match": etag}) assert r2.status_code == 304 def test_latest_excludes_partial_month(): r = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"}) body = r.json() assert body["is_partial_month"] is False assert body["reliability"] in "ABCDE" assert body["period"] >= "2026-06" def test_unknown_geography_404(): r = client.get("/v1/index", params={"geography": "atlantis", "type": "all"}) assert r.status_code == 404 def test_bad_type_422(): r = client.get("/v1/index", params={"geography": "quebec", "type": "castle"}) assert r.status_code == 422 def test_geographies_hierarchy(): r = client.get("/v1/geographies") body = r.json() levels = {g["geography_level"] for g in body["published_series"]} assert levels == {"province", "region", "municipality"} def test_coverage_matrix(): r = client.get("/v1/geographies/coverage", params={"status": "published"}) assert r.status_code == 200 assert len(r.json()["coverage"]) > 0 def test_liquidity(): r = client.get("/v1/liquidity", params={"geography": "montreal", "type": "condo"}) body = r.json() assert body["median_monthly_transactions"] > 400 def test_compare_with_rebase(): r = client.get("/v1/compare", params={ "series": "montreal:condo,quebec-city:condo", "rebase_period": "2022-01"}) body = r.json() assert set(body["series"]) == {"montreal:condo", "quebec-city:condo"} for obs in body["series"].values(): first = [o for o in obs if o["period"] == "2022-01"][0] assert abs(first["index_smoothed"] - 100.0) < 1e-6 def test_map_region_yoy(): r = client.get("/v1/map", params={"metric": "yoy", "level": "region"}) body = r.json() assert len(body["cells"]) == 17 def test_vintages(): r = client.get("/v1/vintages", params={"geography": "quebec", "type": "all"}) assert r.status_code == 200 assert len(r.json()["observations"]) >= 60