osint-dashboard/tests/test_gpsjam.py

151 lines
4.7 KiB
Python
Raw Normal View History

"""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()