osint-dashboard/tests/conftest.py
Sirius DevOps 627990efde Add NASA FIRMS active-fire ingest + /api/fires; API keys management page
Coherent merge of two coordinated features on the shared working tree:

FIRMS fire heatmap (backend, t_6e404c14):
- app/fire_sources.py: fetch FIRMS VIIRS area CSV (free MAP_KEY) -> NATS events.fire
- fires hypertable (TimescaleDB, 1-day chunks) with natural-key PK
  (latitude, longitude, acq_time, satellite); idempotent ON CONFLICT DO NOTHING
- alembic/versions/002_fires.py; GET /api/fires?bbox=&since= (JSON only)
- POST /api/ingest/fires; ~15 min poll loop (FIRMS_INTERVAL=900) in ingester
- env-driven config (FIRMS_MAP_KEY/DATASET/BBOX/INTERVAL); docs/firms.md covers
  the zero-cost GIBS VIIRS_SNPP_Thermal_Anomalies_375m_All tile alternative
- 18 tests (parser, mapping, idempotency, API contract) verified vs real
  TimescaleDB+PostGIS (localhost/osint-dashboard-pg image)

API keys page (frontend, t_4433cff2):
- app/keystore.py: api_keys table (self-creating), FIRMS/GEMINI/TELEGRAM
  registry with format validation, ****last4 masking, get_api_key()
- GET/POST/DELETE /api/keys (never returns full values); Keys tab in index.html

DB_NULL_POOL env switch in app/database.py enables a NullPool for tests /
short-lived processes that open a fresh event loop per unit.
2026-08-24 15:37:42 -04:00

111 lines
3.2 KiB
Python

"""Shared test fixtures.
Unit tests (parsing/mapping) run anywhere. DB-backed tests (idempotency, API)
are marked `integration` and auto-skip when the test database is unreachable —
set DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME to point at a TimescaleDB+PostGIS
instance (e.g. the `localhost/osint-dashboard-pg:test` image) to run them.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
# Make the app package importable from tests (repo root/app).
APP_DIR = Path(__file__).resolve().parent.parent / "app"
if str(APP_DIR) not in sys.path:
sys.path.insert(0, str(APP_DIR))
# Point the app's config at the test database BEFORE any app module is imported
# (database.py builds DATABASE_URL from env at import time).
os.environ.setdefault("DB_HOST", "127.0.0.1")
os.environ.setdefault("DB_PORT", "55432")
os.environ.setdefault("DB_USER", "osint")
os.environ.setdefault("DB_PASSWORD", "osint")
os.environ.setdefault("DB_NAME", "osint_data")
# Each test opens a fresh event loop (asyncio.run); a pooled connection can't be
# reused across loops, so force a NullPool (new connection per session).
os.environ.setdefault("DB_NULL_POOL", "1")
import asyncpg # noqa: E402
pytestmark = []
def _db_reachable() -> bool:
try:
import asyncio
async def _ping() -> bool:
try:
conn = await asyncpg.connect(
host=os.environ["DB_HOST"],
port=int(os.environ["DB_PORT"]),
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
database=os.environ["DB_NAME"],
timeout=3,
)
await conn.close()
return True
except Exception:
return False
return asyncio.run(_ping())
except Exception:
return False
requires_db = pytest.mark.skipif(
not _db_reachable(),
reason="test database unreachable (set DB_* env or start the PG container)",
)
@pytest.fixture()
def clean_fires():
"""Truncate the fires table before and after a DB-backed test."""
import asyncio
async def _truncate():
conn = await asyncpg.connect(
host=os.environ["DB_HOST"],
port=int(os.environ["DB_PORT"]),
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
database=os.environ["DB_NAME"],
)
try:
await conn.execute("TRUNCATE fires")
finally:
await conn.close()
asyncio.run(_truncate())
yield
asyncio.run(_truncate())
def make_fire_msg(**overrides) -> dict:
"""A realistic VIIRS hotspot NATS message (as fire_sources publishes it)."""
msg = {
"source_type": "fire",
"latitude": 39.45678,
"longitude": -121.12345,
"brightness": 341.40,
"confidence": "h",
"acq_time": "2026-08-24T18:10:00+00:00",
"satellite": "N",
"instrument": "VIIRS",
"bright_ti5": 310.20,
"frp": 12.4,
"daynight": "D",
"scan": 0.45,
"track": 0.47,
"version": "2.0NRT",
"raw": None,
}
msg.update(overrides)
return msg