112 lines
3.2 KiB
Python
112 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
|