Phase 1: in-memory ConnectionManager viewport fan-out, 500ms map debounce, cachetools TTLCache, background masscan/ffmpeg, compose memory caps. Phase 2: PostGIS geofences + ST_Intersects alerts, Timescale 1-min CAGGs and timestamp playback, FIRMS/WFIGS x firefighting ADS-B within 20 miles. No Redis/Kafka/Celery.
51 lines
1.4 KiB
Python
51 lines
1.4 KiB
Python
"""Phase 2 API contracts (no Redis; geofences ≠ /api/alerts)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
|
|
from main import app
|
|
|
|
BASE = "http://test"
|
|
|
|
|
|
async def _req(method: str, path: str, **kw) -> httpx.Response:
|
|
transport = httpx.ASGITransport(app=app)
|
|
async with httpx.AsyncClient(transport=transport, base_url=BASE) as client:
|
|
return await client.request(method, path, **kw)
|
|
|
|
|
|
def test_geofence_post_rejects_point():
|
|
resp = asyncio.run(_req(
|
|
"POST", "/api/geofences",
|
|
json={"name": "nope", "geojson": {"type": "Point", "coordinates": [-79, 35]}},
|
|
))
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_geofences_list_does_not_collide_with_alerts():
|
|
resp = asyncio.run(_req("GET", "/api/geofences"))
|
|
assert resp.status_code == 200
|
|
assert isinstance(resp.json(), list)
|
|
|
|
|
|
def test_aircraft_timestamp_returns_list_not_live_error():
|
|
resp = asyncio.run(_req(
|
|
"GET", "/api/aircraft",
|
|
params={"bbox": "-80,35,-78,36", "timestamp": "2026-08-28T12:04:00Z"},
|
|
))
|
|
assert resp.status_code == 200
|
|
assert isinstance(resp.json(), list)
|
|
|
|
|
|
def test_tracks_range_has_bounds():
|
|
body = asyncio.run(_req("GET", "/api/tracks/range")).json()
|
|
assert "min" in body and "max" in body
|
|
|
|
|
|
def test_fire_aircraft_reload_endpoint():
|
|
resp = asyncio.run(_req("GET", "/api/fire-aircraft"))
|
|
assert resp.status_code == 200
|
|
assert isinstance(resp.json(), list)
|