"""GPSJAM GPS-interference overlay: level mapping, CSV→GeoJSON, API contract.""" import asyncio import httpx from live_layers import gpsjam_csv_to_geojson, gpsjam_level, overlay_catalog, _cache from main import app BASE = "http://test" # A valid H3 resolution-4 cell id (the payload hex column carries these). HEX_A = "8400c57ffffffff" CSV = ( "hex,count_good_aircraft,count_bad_aircraft\n" f"{HEX_A},0,20\n" # 100*(20-1)/20 = 95 -> high f"{HEX_A},8,2\n" # 100*(2-1)/10 = 10 -> medium f"{HEX_A},98,2\n" # 100*(2-1)/100 = 1 -> low f"{HEX_A},100,0\n" # bad == 0 -> dropped ) async def _get(path: str) -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url=BASE) as client: return await client.get(path) def test_gpsjam_level_thresholds(): assert gpsjam_level(0.0) == "low" assert gpsjam_level(2.0) == "low" assert gpsjam_level(2.1) == "medium" assert gpsjam_level(10.0) == "medium" assert gpsjam_level(10.1) == "high" assert gpsjam_level(95.0) == "high" def test_gpsjam_csv_to_geojson_levels_and_drop_zero_bad(): fc = gpsjam_csv_to_geojson(CSV) assert fc["type"] == "FeatureCollection" assert len(fc["features"]) == 3 # bad==0 row dropped levels = [f["properties"]["level"] for f in fc["features"]] assert levels == ["high", "medium", "low"] for f in fc["features"]: geom = f["geometry"] assert geom["type"] == "Polygon" ring = geom["coordinates"][0] assert len(ring) == 7 # 6 verts + closing point assert ring[0] == ring[-1] assert f["properties"]["hex"] == HEX_A assert set(f["properties"]).issuperset({"level", "percent_bad", "good", "bad", "hex"}) def test_gpsjam_csv_skips_malformed_rows(): bad_csv = "hex,count_good_aircraft,count_bad_aircraft\n" \ ",1,5\n" \ f"{HEX_A},x,5\n" \ f"{HEX_A},1,notanint\n" \ "not_a_cell,1,5\n" fc = gpsjam_csv_to_geojson(bad_csv) assert fc["features"] == [] def test_overlay_catalog_has_gpsjam_stub(): entry = overlay_catalog()["gpsjam"] assert entry["kind"] == "geojson" assert entry["endpoint"] == "/api/map/gpsjam" assert "GPSJAM" in entry["attribution"] def test_map_gpsjam_returns_featurecollection(monkeypatch): async def fake_fetch(date): return {"type": "FeatureCollection", "features": [{"type": "Feature"}]} monkeypatch.setattr("main.fetch_gpsjam", fake_fetch) resp = asyncio.run(_get("/api/map/gpsjam?date=2026-08-28")) assert resp.status_code == 200 assert resp.json()["type"] == "FeatureCollection" assert "max-age" in (resp.headers.get("cache-control") or "").lower() def test_map_gpsjam_rejects_bad_date(): resp = asyncio.run(_get("/api/map/gpsjam?date=08-28-2026")) assert resp.status_code == 422 def test_map_gpsjam_unavailable_on_404(monkeypatch): import httpx as _httpx async def fake_fetch(date): exc = _httpx.HTTPStatusError( "404", request=_httpx.Request("GET", "http://x"), response=_httpx.Response(404) ) raise exc monkeypatch.setattr("main.fetch_gpsjam", fake_fetch) resp = asyncio.run(_get("/api/map/gpsjam?date=2026-08-28")) assert resp.status_code == 200 body = resp.json() assert body["error"] == "unavailable" assert body["href"] == "https://gpsjam.org/" def test_map_gpsjam_unavailable_on_empty_features(monkeypatch): async def fake_fetch(date): return {"type": "FeatureCollection", "features": []} monkeypatch.setattr("main.fetch_gpsjam", fake_fetch) resp = asyncio.run(_get("/api/map/gpsjam?date=2026-08-28")) assert resp.status_code == 200 assert resp.json()["error"] == "unavailable" def test_fetch_gpsjam_hits_http_once_within_ttl(monkeypatch): _cache.clear() hits = {"n": 0} class FakeResp: text = CSV def raise_for_status(self): pass class FakeClient: def __init__(self, **kw): pass async def __aenter__(self): return self async def __aexit__(self, *exc): return False async def get(self, url): hits["n"] += 1 assert url == "https://gpsjam.org/data/2026-08-28-h3_4.csv" return FakeResp() monkeypatch.setattr("live_layers.httpx.AsyncClient", FakeClient) monkeypatch.setattr("live_layers._http", None) from live_layers import fetch_gpsjam fc1 = asyncio.run(fetch_gpsjam("2026-08-28")) fc2 = asyncio.run(fetch_gpsjam("2026-08-28")) assert len(fc1["features"]) == 3 assert fc2 == fc1 assert hits["n"] == 1 _cache.clear()