All checks were successful
build-and-deploy / build (push) Successful in 2m32s
The Vessels layer now retunes the server-side AISStream subscription to the client viewport instead of a static AISSTREAM_BBOX. The frontend POSTs its quantized viewport box to /api/vessels/subscribe on moveend; the ais_stream worker coalesces and applies it at the service's 1 subscription/s cap, then last-known positions for the new area arrive within a couple of seconds (the frontend does one follow-up fetch after retuning). Bounds the in-memory vessel store across regions. Key stays server-side.
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
"""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
|