SPB Git

spb/qwhpi Public

QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.

Python 63.9% TypeScript 25.4% CSS 5.5% TeX 3.5% SQL 0.8% Makefile 0.5% Dockerfile 0.5%
4.4 KB · 137 lines python
Raw Blame History
1# =============================================================================2# QWHPI — Quebec Weekly Housing Price Index3# Author  : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# File    : api/tests/test_api.py6# Purpose : API tests (monthly v2.1) — every router, ETag / CSV / pagination.7# =============================================================================8"""API test suite (runs against the real canonical monthly parquet)."""910from __future__ import annotations1112import sys13from pathlib import Path1415sys.path.insert(0, str(Path(__file__).resolve().parents[1]))1617from fastapi.testclient import TestClient1819from app.main import app2021client = TestClient(app)222324def test_health():25    r = client.get("/health")26    assert r.status_code == 20027    assert r.json()["status"] == "ok"282930def test_meta_credits_author_and_monthly():31    r = client.get("/v1/meta")32    assert r.status_code == 20033    body = r.json()34    assert body["author"] == "Simon-Pierre Boucher"35    assert body["contact"] == "contact@spboucher.ai"36    assert body["frequency"] == "monthly"37    assert body["series_count"] > 10038    assert body["periods"] >= 60394041def test_index_series_full():42    r = client.get("/v1/index", params={"geography": "quebec-city", "type": "condo"})43    assert r.status_code == 20044    body = r.json()45    assert body["frequency"] == "monthly"46    obs = body["observations"]47    assert len(obs) == body["total_observations"] >= 6048    last = obs[-1]49    for key in ("lower_95", "upper_95", "reliability_grade", "transactions",50                "monthly_pct", "yoy_pct"):51        assert key in last  # uncertainty is never hidden525354def test_index_pagination():55    r = client.get("/v1/index", params={"geography": "quebec", "type": "all",56                                        "limit": 10, "offset": 5})57    body = r.json()58    assert len(body["observations"]) == 1059    assert body["offset"] == 5606162def test_index_csv_export():63    r = client.get("/v1/index", params={"geography": "montreal", "type": "condo",64                                        "format": "csv"})65    assert r.status_code == 20066    assert r.headers["content-type"].startswith("text/csv")67    assert r.text.splitlines()[0].startswith("period")686970def test_index_etag_304():71    r1 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"})72    etag = r1.headers.get("etag")73    assert etag74    r2 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"},75                    headers={"If-None-Match": etag})76    assert r2.status_code == 304777879def test_latest_excludes_partial_month():80    r = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"})81    body = r.json()82    assert body["is_partial_month"] is False83    assert body["reliability"] in "ABCDE"84    assert body["period"] >= "2026-06"858687def test_unknown_geography_404():88    r = client.get("/v1/index", params={"geography": "atlantis", "type": "all"})89    assert r.status_code == 404909192def test_bad_type_422():93    r = client.get("/v1/index", params={"geography": "quebec", "type": "castle"})94    assert r.status_code == 422959697def test_geographies_hierarchy():98    r = client.get("/v1/geographies")99    body = r.json()100    levels = {g["geography_level"] for g in body["published_series"]}101    assert levels == {"province", "region", "municipality"}102103104def test_coverage_matrix():105    r = client.get("/v1/geographies/coverage", params={"status": "published"})106    assert r.status_code == 200107    assert len(r.json()["coverage"]) > 0108109110def test_liquidity():111    r = client.get("/v1/liquidity", params={"geography": "montreal", "type": "condo"})112    body = r.json()113    assert body["median_monthly_transactions"] > 400114115116def test_compare_with_rebase():117    r = client.get("/v1/compare", params={118        "series": "montreal:condo,quebec-city:condo",119        "rebase_period": "2022-01"})120    body = r.json()121    assert set(body["series"]) == {"montreal:condo", "quebec-city:condo"}122    for obs in body["series"].values():123        first = [o for o in obs if o["period"] == "2022-01"][0]124        assert abs(first["index_smoothed"] - 100.0) < 1e-6125126127def test_map_region_yoy():128    r = client.get("/v1/map", params={"metric": "yoy", "level": "region"})129    body = r.json()130    assert len(body["cells"]) == 17131132133def test_vintages():134    r = client.get("/v1/vintages", params={"geography": "quebec", "type": "all"})135    assert r.status_code == 200136    assert len(r.json()["observations"]) >= 60137