osint-dashboard/tests/test_phase2_api.py
Sirius DevOps f6c1cfc454 feat(geofence): watch list, filtered hit log, fence snapshot
Off-viewport clients that send {type:watch_geofences,ids} on /ws/live
still receive geofence_alert; AIS/ADS-B stay viewport-only.

GET /api/geofence-alerts accepts geofence_id/since/until/source_kind.
GET /api/geofences/{id}/at returns CAGG+FIRMS inside the fence at T
(404 if missing, empty lists if DB down). DELETE missing fences 404s.
2026-09-01 01:24:36 -04:00

159 lines
4.5 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)
FENCE_ID = "11111111-1111-1111-1111-111111111111"
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_delete_geofence_404_when_missing(monkeypatch):
async def missing(_gid: str) -> bool:
return False
monkeypatch.setattr("geofence.delete_geofence", missing)
resp = asyncio.run(_req("DELETE", f"/api/geofences/{FENCE_ID}"))
assert resp.status_code == 404
def test_geofence_alerts_passes_filters(monkeypatch):
seen = {}
async def fake_list(**kwargs):
seen.update(kwargs)
return [{"id": "a", "geofence_id": FENCE_ID, "source_kind": "ais"}]
monkeypatch.setattr("geofence.list_alerts", fake_list)
resp = asyncio.run(_req(
"GET", "/api/geofence-alerts",
params={
"geofence_id": FENCE_ID,
"since": "2026-08-28T00:00:00Z",
"until": "2026-08-29T00:00:00Z",
"source_kind": "ais",
"limit": 10,
},
))
assert resp.status_code == 200
assert resp.json()[0]["source_kind"] == "ais"
assert seen["geofence_id"] == FENCE_ID
assert seen["source_kind"] == "ais"
assert seen["limit"] == 10
assert seen["since"] is not None
assert seen["until"] is not None
def test_geofence_alerts_rejects_bad_source_kind():
resp = asyncio.run(_req(
"GET", "/api/geofence-alerts", params={"source_kind": "camera"},
))
assert resp.status_code == 422
def test_geofence_at_404_when_missing(monkeypatch):
async def no_snap(gid: str, ts):
return None
monkeypatch.setattr("geofence.snapshot_at", no_snap)
resp = asyncio.run(_req(
"GET", f"/api/geofences/{FENCE_ID}/at",
params={"timestamp": "2026-08-28T12:04:00Z"},
))
assert resp.status_code == 404
def test_geofence_at_empty_lists_when_db_down(monkeypatch):
async def empty_snap(gid: str, ts):
return {
"geofence_id": gid,
"timestamp": ts.isoformat(),
"aircraft": [],
"vessels": [],
"fires": [],
}
monkeypatch.setattr("geofence.snapshot_at", empty_snap)
resp = asyncio.run(_req(
"GET", f"/api/geofences/{FENCE_ID}/at",
params={"timestamp": "2026-08-28T12:04:00Z"},
))
assert resp.status_code == 200
body = resp.json()
assert body["geofence_id"] == FENCE_ID
assert body["aircraft"] == []
assert body["vessels"] == []
assert body["fires"] == []
assert "timestamp" in body
def test_geofence_at_does_not_notify(monkeypatch):
called = {"notify": 0}
async def empty_snap(gid: str, ts):
return {
"geofence_id": gid,
"timestamp": ts.isoformat(),
"aircraft": [],
"vessels": [],
"fires": [],
}
async def boom(**_kw):
called["notify"] += 1
raise AssertionError("GET /at must not record_and_notify")
monkeypatch.setattr("geofence.snapshot_at", empty_snap)
monkeypatch.setattr("geofence.record_and_notify", boom)
resp = asyncio.run(_req(
"GET", f"/api/geofences/{FENCE_ID}/at",
params={"timestamp": "2026-08-28T12:04:00Z"},
))
assert resp.status_code == 200
assert called["notify"] == 0
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)