"""Discord HTTP-interactions: Ed25519 verification, PING/PONG, /grade dispatch, and the wired FastAPI route. No live Discord — we hold a test signing key and sign requests ourselves.""" import datetime import json import pytest from fastapi.testclient import TestClient from nacl.signing import SigningKey from web import app as appmod from notifications import discord_interactions as di from web import homepage B = "/thermograph" _SK = SigningKey.generate() _PUBKEY = _SK.verify_key.encode().hex() def _sign(body: bytes, ts: str = "1700000000") -> tuple[str, str]: return _SK.sign(ts.encode() + body).signature.hex(), ts _CARD = { "display": "Tokyo, Japan", "name": "Tokyo", "slug": "tokyo-jp", "metric_label": "High temp", "value": 96.0, "normal": 84.0, "pct_label": "97th percentile", "grade_label": "Very High", "tail": "warm", "date_label": None, } # --- signature verification -------------------------------------------------- def test_verify_accepts_a_good_signature(): body = b'{"type":1}' sig, ts = _sign(body) assert di.verify(sig, ts, body, _PUBKEY) is True def test_verify_rejects_tampering_and_bad_inputs(): body = b'{"type":1}' sig, ts = _sign(body) assert di.verify(sig, ts, body + b" ", _PUBKEY) is False # body changed assert di.verify(sig, "1700000001", body, _PUBKEY) is False # timestamp changed assert di.verify("00" * 64, ts, body, _PUBKEY) is False # wrong signature assert di.verify("nothex", ts, body, _PUBKEY) is False # malformed assert di.verify(sig, ts, body, "") is False # no key configured assert di.verify("", ts, body, _PUBKEY) is False # no signature # --- handle(): routing ------------------------------------------------------- def test_handle_ping_pongs(monkeypatch): monkeypatch.setattr(di, "PUBLIC_KEY", _PUBKEY) body = b'{"type":1}' sig, ts = _sign(body) status, payload = di.handle(body, sig, ts) assert status == 200 and payload == {"type": 1} def test_handle_rejects_bad_signature(monkeypatch): monkeypatch.setattr(di, "PUBLIC_KEY", _PUBKEY) body = b'{"type":1}' status, payload = di.handle(body, "00" * 64, "1700000000") assert status == 401 def test_grade_command_returns_the_citys_grade(monkeypatch): monkeypatch.setattr(di, "PUBLIC_KEY", _PUBKEY) monkeypatch.setattr(homepage, "_grade_city", lambda city, today: dict(_CARD)) body = json.dumps({"type": 2, "data": { "name": "grade", "options": [{"name": "city", "value": "Tokyo"}]}}).encode() sig, ts = _sign(body) status, payload = di.handle(body, sig, ts) assert status == 200 and payload["type"] == 4 embed = payload["data"]["embeds"][0] assert "Tokyo, Japan" in embed["title"] assert "96°F" in embed["description"] and "97th percentile" in embed["description"] assert embed["url"].endswith("/climate/tokyo-jp") def test_grade_unknown_city_is_ephemeral(monkeypatch): monkeypatch.setattr(di, "PUBLIC_KEY", _PUBKEY) data = di._grade_message("Nowherecityville") assert data["flags"] == di._EPHEMERAL and "don't track" in data["content"] def test_grade_city_not_yet_warm_is_ephemeral(monkeypatch): monkeypatch.setattr(homepage, "_grade_city", lambda city, today: None) data = di._grade_message("Tokyo") # a real curated city, but no warm cache assert data["flags"] == di._EPHEMERAL and "No recent reading" in data["content"] def test_resolve_city_prefers_exact_then_population(): # Real cities.json: an exact name resolves, and it's a real curated entry. tokyo = di._resolve_city("tokyo") assert tokyo is not None and tokyo["name"] == "Tokyo" assert di._resolve_city(" ") is None assert di._resolve_city("Zzznotacity") is None # --- the wired FastAPI route ------------------------------------------------- def test_interactions_route_pings_and_rejects(monkeypatch): monkeypatch.setattr(di, "PUBLIC_KEY", _PUBKEY) client = TestClient(appmod.app) body = b'{"type":1}' sig, ts = _sign(body) # Signed PING -> PONG. Send raw bytes so the signature matches byte-for-byte. r = client.post(f"{B}/discord/interactions", content=body, headers={"x-signature-ed25519": sig, "x-signature-timestamp": ts}) assert r.status_code == 200 and r.json() == {"type": 1} # Unsigned -> 401 (Discord's security probe). r = client.post(f"{B}/discord/interactions", content=body) assert r.status_code == 401