osint-dashboard/tests/test_satellites.py

201 lines
6.8 KiB
Python
Raw Normal View History

"""CelesTrak satellites overlay: GP JSON parser, 2h cache, groups, bbox."""
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
import httpx
import satellites
from live_layers import _cache, overlay_catalog
from main import app
BASE = "http://test"
# Two real CelesTrak GP JSON records (trimmed to the OMM fields sgp4 needs).
ISS = {
"OBJECT_NAME": "ISS (ZARYA)", "OBJECT_ID": "1998-067A",
"EPOCH": "2026-08-31T11:11:23.184384", "MEAN_MOTION": 15.4894954,
"ECCENTRICITY": 0.00050456, "INCLINATION": 51.6314,
"RA_OF_ASC_NODE": 287.5025, "ARG_OF_PERICENTER": 92.8598,
"MEAN_ANOMALY": 267.2968, "EPHEMERIS_TYPE": 0,
"CLASSIFICATION_TYPE": "U", "NORAD_CAT_ID": 25544,
"ELEMENT_SET_NO": 999, "REV_AT_EPOCH": 58342,
"BSTAR": 9.9862358e-5, "MEAN_MOTION_DOT": 5.046e-5,
"MEAN_MOTION_DDOT": 0,
}
HST = {
"OBJECT_NAME": "HST", "OBJECT_ID": "1990-037B",
"EPOCH": "2026-08-31T11:11:23.184384", "MEAN_MOTION": 15.0865888,
"ECCENTRICITY": 0.0002426, "INCLINATION": 28.4697,
"RA_OF_ASC_NODE": 102.1854, "ARG_OF_PERICENTER": 152.8462,
"MEAN_ANOMALY": 207.2795, "EPHEMERIS_TYPE": 0,
"CLASSIFICATION_TYPE": "U", "NORAD_CAT_ID": 20580,
"ELEMENT_SET_NO": 999, "REV_AT_EPOCH": 12345,
"BSTAR": 2.9e-5, "MEAN_MOTION_DOT": 0.0,
"MEAN_MOTION_DDOT": 0,
}
FIXTURE = [ISS, HST]
NOW = datetime(2026, 8, 31, 12, 0, 0, tzinfo=timezone.utc)
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)
def test_propagate_gp_shape():
rows = satellites.propagate_gp(FIXTURE, "stations", NOW)
assert len(rows) == 2
by_id = {r["id"]: r for r in rows}
assert set(by_id) == {"25544", "20580"}
iss = by_id["25544"]
assert iss["name"] == "ISS (ZARYA)"
assert iss["group"] == "stations"
# ISS is in LEO: ~400 km, |lat| <= inclination 51.63, lon in range.
assert 300 < iss["alt_km"] < 500
assert -51.7 <= iss["lat"] <= 51.7
assert -180 <= iss["lon"] <= 180
for key in ("id", "name", "lat", "lon", "alt_km", "group"):
assert key in iss
def test_propagate_gp_skips_malformed():
bad = [{"OBJECT_NAME": "x"}, None, 42, {"NORAD_CAT_ID": 1}]
assert satellites.propagate_gp(bad, "stations", NOW) == []
def test_parse_groups_defaults_and_validation():
assert satellites.parse_groups("stations,weather") == ["stations", "weather"]
assert satellites.parse_groups("weather,gps-ops") == ["weather", "gps-ops"]
# starlink is allowed only when explicitly requested
assert satellites.parse_groups("starlink") == ["starlink"]
assert satellites.parse_groups("stations,stations") == ["stations"]
for bad in ("", None, "debris", "stations,active", "stations, weather, active"):
try:
satellites.parse_groups(bad)
except ValueError:
pass
else:
raise AssertionError(f"expected ValueError for {bad!r}")
def test_overlay_catalog_has_satellites_stub():
entry = overlay_catalog()["satellites"]
assert entry["kind"] == "points"
assert entry["endpoint"] == "/api/satellites"
assert "CelesTrak" in entry["attribution"]
def test_unknown_group_400():
resp = asyncio.run(_get("/api/satellites?groups=debris"))
assert resp.status_code == 400
def test_default_groups_ok_with_fake_fetch(monkeypatch):
async def fake(groups, bbox=None, limit=2000):
return {"satellites": [], "source": "celestrak",
"tle_epoch": None, "timestamp": "t"}
monkeypatch.setattr("main.fetch_satellites", fake)
resp = asyncio.run(_get("/api/satellites"))
assert resp.status_code == 200
body = resp.json()
assert body["source"] == "celestrak"
assert "max-age" in (resp.headers.get("cache-control") or "").lower()
def test_2h_cache_does_not_refetch(monkeypatch):
satellites._last_good.clear()
_cache.clear()
hits = {"n": 0}
class FakeResp:
def __init__(self, data):
self._data = data
def raise_for_status(self):
pass
def json(self):
return self._data
class FakeClient:
def __init__(self):
pass
async def get(self, url, params=None, headers=None):
hits["n"] += 1
assert "celestrak.org/NORAD/elements/gp.php" in url
return FakeResp(FIXTURE)
monkeypatch.setattr("live_layers._http", FakeClient())
async def run():
p1 = await satellites.fetch_satellites(["stations"])
p2 = await satellites.fetch_satellites(["stations"])
return p1, p2
p1, p2 = asyncio.run(run())
assert len(p1["satellites"]) == 2
assert p1["tle_epoch"] == "2026-08-31T11:11:23.184384"
# Same element blob served from cache (no refetch), same ids/epochs.
assert [s["id"] for s in p2["satellites"]] == [s["id"] for s in p1["satellites"]]
assert p2["tle_epoch"] == p1["tle_epoch"]
assert hits["n"] == 1
_cache.clear()
satellites._last_good.clear()
def test_bbox_culls_satellites(monkeypatch):
"""bbox filtering in fetch_satellites, deterministic via fake propagation."""
async def fake_elements(group):
return [{"x": 1}], "2026-08-31T11:11:23.184384"
monkeypatch.setattr("satellites._group_elements", fake_elements)
def fake_propagate(elements, group, now):
return [
{"id": "a", "name": "A", "lat": 10.0, "lon": 20.0, "alt_km": 400.0, "group": group},
{"id": "b", "name": "B", "lat": 45.0, "lon": -70.0, "alt_km": 400.0, "group": group},
{"id": "c", "name": "C", "lat": -10.0, "lon": 30.0, "alt_km": 400.0, "group": group},
]
monkeypatch.setattr("satellites.propagate_gp", fake_propagate)
payload = asyncio.run(
satellites.fetch_satellites(["stations"], bbox="-80,0,-60,50")
)
ids = [s["id"] for s in payload["satellites"]]
assert ids == ["b"] # only (45, -70) falls inside the box
def test_bbox_culls_nothing_when_empty():
from satellites import fetch_satellites
# No bbox: all rows returned up to limit.
# (skip network; just sanity-check the arg is accepted by signature)
assert callable(fetch_satellites)
def test_satnogs_fallback_parser():
payload = [{
"tle0": "0 ISS (ZARYA)",
"tle1": "1 25544U 98067A 26243.85334329 .00004554 00000-0 90917-4 0 9992",
"tle2": "2 25544 51.6312 285.5873 0005057 94.2999 265.8567 15.48953200583481",
"norad_cat_id": 25544,
"updated": "2026-09-01T01:19:54.327653Z",
}]
rows, epoch = satellites.propagate_satnogs_tle(payload, "stations", NOW)
assert len(rows) == 1
row = rows[0]
assert row["id"] == "25544"
assert row["name"] == "ISS (ZARYA)"
assert row["group"] == "stations"
assert epoch == "2026-09-01T01:19:54.327653Z"
assert 300 < row["alt_km"] < 500