osint-dashboard/tests/test_api_live_layers.py

110 lines
3.8 KiB
Python
Raw Permalink Normal View History

"""API contract tests for live overlay endpoints (no DB required)."""
import asyncio
import httpx
from live_layers import overlay_catalog
from main import app
BASE = "http://test"
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)
async def _post(path: str, payload: dict | None) -> httpx.Response:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url=BASE) as client:
return await client.post(path, json=payload)
def test_map_layers_includes_overlays():
body = asyncio.run(_get("/api/map/layers")).json()
assert "layers" in body
overlays = body["overlays"]
for key in ("radar_iem", "radar_rainviewer", "gibs_thermal",
"aircraft", "vessels", "trains", "nws_alerts",
"wfigs_incidents", "wfigs_perimeters"):
assert key in overlays
assert overlay_catalog()["radar_iem"]["tileUrl"].startswith("https://mesonet")
def test_aircraft_requires_bbox():
resp = asyncio.run(_get("/api/aircraft"))
assert resp.status_code == 422
def test_vessels_empty_without_ais_key():
resp = asyncio.run(_get("/api/vessels"))
assert resp.status_code == 200
assert resp.json() == []
assert "max-age" in (resp.headers.get("cache-control") or "").lower()
def _desired_boxes():
import asyncio as _a
from ais_stream import _take_desired_boxes
return _a.run(_take_desired_boxes())
def test_vessels_subscribe_sets_viewport_box():
assert _desired_boxes() is None
resp = asyncio.run(_post("/api/vessels/subscribe", {"bbox": "-70,40,-60,45"}))
assert resp.status_code == 200
body = resp.json()
assert body["ok"] is True and body["bbox"] == "-70,40,-60,45"
# AISStream corner order: [[lat, lon], [lat, lon]] (southwest, northeast).
assert _desired_boxes() == [[[40.0, -70.0], [45.0, -60.0]]]
def test_vessels_subscribe_empty_resets():
assert asyncio.run(_post("/api/vessels/subscribe", {"bbox": "-70,40,-60,45"})).status_code == 200
resp = asyncio.run(_post("/api/vessels/subscribe", {"bbox": ""}))
assert resp.status_code == 200
assert resp.json()["bbox"] is None
assert _desired_boxes() is None
def test_vessels_subscribe_rejects_bad_bbox():
for bad in ("1,2,3", "a,b,c,d", "20,30,10,40", "0,0,0,200"):
resp = asyncio.run(_post("/api/vessels/subscribe", {"bbox": bad}))
assert resp.status_code == 422, bad
# Explicit null bbox is the "reset to env default" path (still 200).
assert asyncio.run(_post("/api/vessels/subscribe", {"bbox": None})).status_code == 200
def test_aircraft_photo_requires_hex_or_reg():
assert asyncio.run(_get("/api/aircraft/photo")).status_code == 422
def test_aircraft_photo_rejects_bad_hex():
# hex must be exactly 6 hex chars
resp = asyncio.run(_get("/api/aircraft/photo?hex=xyz1234"))
assert resp.status_code == 422
def test_aircraft_photo_returns_photo(monkeypatch):
async def fake(hex_code=None, reg=None):
return {"id": "1", "src": "https://t.plnspttrs.net/a_280.jpg",
"link": "https://www.planespotters.net/photo/1/x", "photographer": "A"}
monkeypatch.setattr("main.fetch_planespotters_photo", fake)
resp = asyncio.run(_get("/api/aircraft/photo?hex=e8027e"))
assert resp.status_code == 200
body = resp.json()
assert body["src"].startswith("https://t.plnspttrs.net/")
assert "max-age" in (resp.headers.get("cache-control") or "").lower()
def test_aircraft_photo_404_when_no_photo(monkeypatch):
async def fake(hex_code=None, reg=None):
return None
monkeypatch.setattr("main.fetch_planespotters_photo", fake)
resp = asyncio.run(_get("/api/aircraft/photo?reg=D-ABCD"))
assert resp.status_code == 404