diff --git a/app/fire_sources.py b/app/fire_sources.py index 192b28a..920c045 100644 --- a/app/fire_sources.py +++ b/app/fire_sources.py @@ -25,12 +25,14 @@ import csv import io import json import logging +import os from datetime import datetime, timezone import httpx import nats -from config import FIRMS_MAP_KEY, FIRMS_DATASET, FIRMS_BBOX, FIRMS_TIMEOUT, NATS_URL +from config import FIRMS_DATASET, FIRMS_BBOX, FIRMS_TIMEOUT, NATS_URL +from keystore import get_api_key logger = logging.getLogger("osint.firms") @@ -159,16 +161,21 @@ async def ingest_fires(bbox: str | None = None) -> int: warning when FIRMS_MAP_KEY is not set, so the ingester keeps running for the other sources. """ - if not FIRMS_MAP_KEY: + # Key resolution: env var first (12-factor), then the shared Postgres + # api_keys table so a key saved via the dashboard Keys page is picked up + # on the next poll without a restart. + map_key = os.getenv("FIRMS_MAP_KEY", "") or await get_api_key("FIRMS_MAP_KEY") + if not map_key: logger.warning( "FIRMS_MAP_KEY not set — fire ingest disabled. " - "Get a free key at https://firms.modaps.eosdis.nasa.gov/api/map_key_info/" + "Set it in the dashboard Keys page or .env. " + "Free key: https://firms.modaps.eosdis.nasa.gov/api/map_key_info/" ) return 0 area = bbox or FIRMS_BBOX url = FIRMS_AREA_CSV.format( - key=FIRMS_MAP_KEY, dataset=FIRMS_DATASET, bbox=area + key=map_key, dataset=FIRMS_DATASET, bbox=area ) async with httpx.AsyncClient(timeout=FIRMS_TIMEOUT) as client: diff --git a/tests/test_fire_sources.py b/tests/test_fire_sources.py index 10536f0..ec13b1e 100644 --- a/tests/test_fire_sources.py +++ b/tests/test_fire_sources.py @@ -73,6 +73,65 @@ def test_parse_firms_csv_empty_and_garbage(): def test_ingest_fires_idles_without_map_key(monkeypatch): - # No key -> returns 0 without attempting a network call. - monkeypatch.setattr("fire_sources.FIRMS_MAP_KEY", "") + # No key anywhere -> returns 0 without attempting a network call. + monkeypatch.setenv("FIRMS_MAP_KEY", "") + monkeypatch.setattr( + "fire_sources.get_api_key", lambda name: _async_none() + ) assert asyncio.run(ingest_fires()) == 0 + + +def test_ingest_fires_uses_keystore_key(monkeypatch): + # Key saved via the dashboard Keys page (Postgres) is picked up. + monkeypatch.setenv("FIRMS_MAP_KEY", "") + monkeypatch.setattr( + "fire_sources.get_api_key", + lambda name: _async_return("a" * 32), + ) + + captured = {} + + class FakeResp: + status_code = 200 + text = SAMPLE_CSV + + def raise_for_status(self): + pass + + class FakeClient: + def __init__(self, **kw): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + return False + + async def get(self, url): + captured["url"] = url + return FakeResp() + + monkeypatch.setattr("fire_sources.httpx.AsyncClient", FakeClient) + + published = [] + + async def fake_publish(points): + published.extend(points) + return len(points) + + monkeypatch.setattr("fire_sources.publish_fire_batch", fake_publish) + + assert asyncio.run(ingest_fires()) == 5 + assert "a" * 32 in captured["url"] + + +def _async_return(value): + async def inner(): + return value + + return inner() + + +def _async_none(): + return _async_return(None)