"""The internal control surface the thermograph-daemon Go binary calls back into (api/internal_routes.py): shared-secret gating (fail closed when the token was never provisioned), the gateway-ready /grade reply with the interactions-only ephemeral flag dropped, the two job triggers, and the one-in-flight-run guard. The underlying job/grade functions are stubbed — hermetic, no real network.""" import pytest from fastapi.testclient import TestClient from api import internal_routes from notifications import discord_interactions as di from web import app as appmod TOKEN = "test-internal-token" HDR = {"X-Thermograph-Internal-Token": TOKEN} @pytest.fixture def client(monkeypatch): monkeypatch.setenv("THERMOGRAPH_INTERNAL_TOKEN", TOKEN) return TestClient(appmod.app) @pytest.fixture def grade_stub(monkeypatch): """Reply payload for the shared grade builder, mirroring the real shape (embeds + an ephemeral flag the gateway path must drop).""" calls = [] monkeypatch.setattr( di, "_grade_message", lambda query: calls.append(query) or {"embeds": [{"title": f"grade:{query}"}], "flags": 64}) return calls # --- auth: fail closed, then reject before accept ---------------------------- def test_router_is_disabled_when_no_token_is_provisioned(monkeypatch): """No THERMOGRAPH_INTERNAL_TOKEN => the surface doesn't exist (404), even for a caller presenting a header — never fall open to no-auth.""" monkeypatch.delenv("THERMOGRAPH_INTERNAL_TOKEN", raising=False) client = TestClient(appmod.app) assert client.post("/internal/discord/grade", json={"query": "x"}, headers=HDR).status_code == 404 assert client.post("/internal/jobs/warm-cities", json={}, headers=HDR).status_code == 404 assert client.post("/internal/jobs/indexnow", json={}, headers=HDR).status_code == 404 def test_missing_header_is_rejected(client): assert client.post("/internal/jobs/indexnow", json={}).status_code == 401 def test_wrong_token_is_rejected(client): r = client.post("/internal/jobs/indexnow", json={}, headers={"X-Thermograph-Internal-Token": "not-the-token"}) assert r.status_code == 401 def test_correct_token_is_accepted(client, monkeypatch): import indexnow monkeypatch.setattr(indexnow, "submit_if_changed", lambda: None) assert client.post("/internal/jobs/indexnow", json={}, headers=HDR).status_code == 200 # --- /internal/discord/grade ------------------------------------------------- def test_grade_returns_the_shared_builder_message_without_ephemeral_flag(client, grade_stub): r = client.post("/internal/discord/grade", json={"query": "Phoenix"}, headers=HDR) assert r.status_code == 200 # Gateway messages can't be ephemeral — the flag must be gone, everything # else exactly as _grade_message built it. assert r.json() == {"embeds": [{"title": "grade:Phoenix"}]} assert grade_stub == ["Phoenix"] def test_grade_requires_a_query(client, grade_stub): assert client.post("/internal/discord/grade", json={}, headers=HDR).status_code == 422 assert grade_stub == [] # --- job triggers ------------------------------------------------------------ def test_warm_cities_invokes_warm_cities_main(client, monkeypatch): import warm_cities calls = [] monkeypatch.setattr(warm_cities, "main", lambda: calls.append(1)) r = client.post("/internal/jobs/warm-cities", json={}, headers=HDR) assert r.status_code == 200 assert r.json() == {"ok": True} assert calls == [1] def test_indexnow_invokes_submit_if_changed(client, monkeypatch): import indexnow calls = [] monkeypatch.setattr(indexnow, "submit_if_changed", lambda: calls.append(1)) r = client.post("/internal/jobs/indexnow", json={}, headers=HDR) assert r.status_code == 200 assert r.json() == {"ok": True} assert calls == [1] # --- one-in-flight-run guard ------------------------------------------------- def test_concurrent_job_invocation_answers_409(client, monkeypatch): """A second trigger while a run is in flight must not stack — warm_cities spends the Open-Meteo quota, so a stacked run double-spends it. Holding the job's lock stands in for an in-flight run.""" import indexnow monkeypatch.setattr(indexnow, "submit_if_changed", lambda: None) lock = internal_routes._JOB_LOCKS["warm-cities"] assert lock.acquire(blocking=False) try: assert client.post("/internal/jobs/warm-cities", json={}, headers=HDR).status_code == 409 # The guard is per job: an in-flight warm run doesn't block indexnow. assert client.post("/internal/jobs/indexnow", json={}, headers=HDR).status_code == 200 finally: lock.release() # Released => the next trigger runs again. import warm_cities monkeypatch.setattr(warm_cities, "main", lambda: None) assert client.post("/internal/jobs/warm-cities", json={}, headers=HDR).status_code == 200 def test_job_lock_is_released_after_a_failing_run(monkeypatch): """A crashed run must not wedge the job forever: the 500 surfaces, the lock is released, and the next trigger runs.""" monkeypatch.setenv("THERMOGRAPH_INTERNAL_TOKEN", TOKEN) client = TestClient(appmod.app, raise_server_exceptions=False) import indexnow monkeypatch.setattr(indexnow, "submit_if_changed", lambda: (_ for _ in ()).throw(RuntimeError("boom"))) assert client.post("/internal/jobs/indexnow", json={}, headers=HDR).status_code == 500 monkeypatch.setattr(indexnow, "submit_if_changed", lambda: None) assert client.post("/internal/jobs/indexnow", json={}, headers=HDR).status_code == 200