"""GET /api/conflicts — curated conflict-zone catalog + event-count roll-up. No outbound HTTP: event counts come from geocoded rows already (or not) in the DB, and the API tests monkeypatch ``main._fetch_geocoded_points`` so no database is required for the contract checks. """ from datetime import datetime, timezone import httpx from conflicts import SEVERITIES, conflict_zones, zone_event_stats from live_layers import overlay_catalog from main import app BASE = "http://test" def _get(path: str, monkeypatch=None, points=None) -> httpx.Response: import asyncio async def run() -> httpx.Response: if monkeypatch is not None: async def fake(): return points or [] monkeypatch.setattr("main._fetch_geocoded_points", fake) transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url=BASE) as client: return await client.get(path) return asyncio.run(run()) # ── Catalog shape ────────────────────────────────────────────────────── def test_catalog_length(): zones = conflict_zones() assert len(zones) == 13 def test_catalog_severity_enum(): zones = conflict_zones() sevs = {z["severity"] for z in zones} assert sevs.issubset(SEVERITIES) # All three tiers are represented. assert sevs == SEVERITIES def test_catalog_fields_factual_and_complete(): zones = conflict_zones() ids = [z["id"] for z in zones] assert len(set(ids)) == len(ids) # unique ids for z in zones: assert z["label"] assert z["description"].strip() assert -90.0 <= z["lat"] <= 90.0 assert -180.0 <= z["lon"] <= 180.0 # internal-only bbox is well-formed: (min_lat, min_lon, max_lat, max_lon) min_lat, min_lon, max_lat, max_lon = z["bbox"] assert min_lat <= max_lat and min_lon <= max_lon assert min_lat <= z["lat"] <= max_lat and min_lon <= z["lon"] <= max_lon def test_overlay_catalog_has_conflicts(): entry = overlay_catalog()["conflicts"] assert entry["kind"] == "points" assert entry["endpoint"] == "/api/conflicts" # ── Pure counting ────────────────────────────────────────────────────── TS1 = datetime(2026, 8, 30, 12, 0, tzinfo=timezone.utc) TS2 = datetime(2026, 8, 30, 13, 0, tzinfo=timezone.utc) def test_zone_event_stats_counts_and_picks_latest(): bbox = (40.0, 20.0, 52.0, 40.0) # roughly Ukraine points = [ (50.45, 30.52, TS1), # inside (48.0, 25.0, TS2), # inside, later (0.0, -60.0, TS1), # outside (15.0, 45.0, TS2), # outside (lat ok, lon out) ] count, latest = zone_event_stats(points, bbox) assert count == 2 assert latest == TS2 def test_zone_event_stats_empty_bbox(): count, latest = zone_event_stats([], (0.0, 0.0, 1.0, 1.0)) assert count == 0 assert latest is None # ── API contract (mocked map items, no DB) ───────────────────────────── def test_conflicts_returns_catalog_with_mocked_counts(monkeypatch): points = [ (50.45, 30.52, TS1), # Ukraine (25.03, 121.56, TS2), # Taiwan Strait (0.0, -60.0, TS1), # nowhere ] resp = _get("/api/conflicts", monkeypatch=monkeypatch, points=points) assert resp.status_code == 200 body = resp.json() assert "zones" in body and "timestamp" in body by_id = {z["id"]: z for z in body["zones"]} assert len(body["zones"]) == 13 zone = by_id["ukraine"] assert zone["eventCount"] == 1 assert zone["lastUpdated"] == TS1.isoformat().replace("+00:00", "Z") assert zone["severity"] == "war" assert by_id["taiwan_strait"]["eventCount"] == 1 assert by_id["gaza"]["eventCount"] == 0 # exact per-zone key contract the frontend consumes assert set(zone.keys()) == { "id", "label", "severity", "lat", "lon", "description", "eventCount", "lastUpdated", } def test_conflicts_empty_db_yields_zero_counts(monkeypatch): resp = _get("/api/conflicts", monkeypatch=monkeypatch, points=[]) assert resp.status_code == 200 body = resp.json() assert all(z["eventCount"] == 0 for z in body["zones"]) assert all(z["lastUpdated"] is None for z in body["zones"])