125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
"""Discord gateway bot: the message-handling logic that decides when to reply and
|
|
with what. No live gateway — we feed MESSAGE_CREATE payloads straight to the pure
|
|
handler and stub the shared /grade lookup so these stay data-independent."""
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from notifications import discord
|
|
from notifications import discord_bot as bot
|
|
from notifications import discord_interactions as di
|
|
|
|
BOT_ID = "999"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _stub_grade(monkeypatch):
|
|
"""Reply payload for /grade, mirroring the real shape (embeds + an ephemeral
|
|
flag the gateway path must drop)."""
|
|
monkeypatch.setattr(
|
|
di, "_grade_message",
|
|
lambda query: {"embeds": [{"title": f"grade:{query}"}], "flags": 64})
|
|
|
|
|
|
def _msg(content="", *, author_id="1", bot_author=False, mentions=(), guild=True):
|
|
m = {
|
|
"content": content,
|
|
"id": "42",
|
|
"channel_id": "77",
|
|
"author": {"id": author_id, "bot": bot_author},
|
|
"mentions": [{"id": mid} for mid in mentions],
|
|
}
|
|
if guild:
|
|
m["guild_id"] = "5"
|
|
return m
|
|
|
|
|
|
# --- when the bot stays silent ----------------------------------------------
|
|
|
|
def test_ignores_other_bots():
|
|
assert bot._response_for_message(
|
|
_msg("<@999> Phoenix", author_id="8", bot_author=True, mentions=[BOT_ID]), BOT_ID) is None
|
|
|
|
|
|
def test_ignores_its_own_messages():
|
|
assert bot._response_for_message(
|
|
_msg("hello", author_id=BOT_ID, mentions=[BOT_ID]), BOT_ID) is None
|
|
|
|
|
|
def test_ignores_guild_message_without_a_mention():
|
|
assert bot._response_for_message(_msg("just chatting", mentions=[]), BOT_ID) is None
|
|
|
|
|
|
# --- when the bot replies ----------------------------------------------------
|
|
|
|
def test_mention_with_city_returns_grade_without_ephemeral_flag():
|
|
out = bot._response_for_message(_msg("<@999> Phoenix", mentions=[BOT_ID]), BOT_ID)
|
|
assert out == {"embeds": [{"title": "grade:Phoenix"}]} # flags dropped
|
|
assert "flags" not in out
|
|
|
|
|
|
def test_legacy_nickname_mention_is_stripped():
|
|
out = bot._response_for_message(_msg("<@!999> Tokyo", mentions=[BOT_ID]), BOT_ID)
|
|
assert out["embeds"][0]["title"] == "grade:Tokyo"
|
|
|
|
|
|
def test_dm_needs_no_mention():
|
|
out = bot._response_for_message(_msg("Berlin", guild=False, mentions=[]), BOT_ID)
|
|
assert out["embeds"][0]["title"] == "grade:Berlin"
|
|
|
|
|
|
def test_mention_with_no_query_gives_help():
|
|
out = bot._response_for_message(_msg("<@999>", mentions=[BOT_ID]), BOT_ID)
|
|
assert "content" in out and "mention me" in out["content"].lower()
|
|
|
|
|
|
# --- helpers -----------------------------------------------------------------
|
|
|
|
def test_strip_mentions_handles_both_forms():
|
|
assert bot._strip_mentions("<@999> <@!999> Sao Paulo", BOT_ID) == "Sao Paulo"
|
|
|
|
|
|
def test_is_dm_is_true_without_guild_id():
|
|
assert bot._is_dm(_msg("x", guild=False)) is True
|
|
assert bot._is_dm(_msg("x", guild=True)) is False
|
|
|
|
|
|
# --- _dispatch: same reply, off the event loop -------------------------------
|
|
|
|
def test_dispatch_delivers_the_same_reply_via_a_thread(monkeypatch):
|
|
"""_dispatch now runs _response_for_message through asyncio.to_thread (the
|
|
event-loop fix) -- confirm that still produces the exact same reply and gets
|
|
it to _send_reply, i.e. the offload didn't change behaviour."""
|
|
sent = {}
|
|
|
|
async def _fake_send_reply(channel_id, message_id, data):
|
|
sent["channel_id"] = channel_id
|
|
sent["message_id"] = message_id
|
|
sent["data"] = data
|
|
|
|
monkeypatch.setattr(bot, "_send_reply", _fake_send_reply)
|
|
sess = bot._Session()
|
|
sess.user_id = BOT_ID
|
|
msg = {"t": "MESSAGE_CREATE", "d": _msg("<@999> Phoenix", mentions=[BOT_ID])}
|
|
|
|
asyncio.run(bot._dispatch(msg, sess))
|
|
|
|
assert sent["channel_id"] == "77"
|
|
assert sent["message_id"] == "42"
|
|
assert sent["data"] == {"embeds": [{"title": "grade:Phoenix"}]}
|
|
|
|
|
|
# --- enabled() gating --------------------------------------------------------
|
|
|
|
def test_enabled_is_off_by_default(monkeypatch):
|
|
monkeypatch.delenv("THERMOGRAPH_DISCORD_BOT", raising=False)
|
|
monkeypatch.setattr(discord, "BOT_TOKEN", "a-token")
|
|
assert bot.enabled() is False
|
|
|
|
|
|
def test_enabled_needs_both_flag_and_token(monkeypatch):
|
|
monkeypatch.setenv("THERMOGRAPH_DISCORD_BOT", "1")
|
|
monkeypatch.setattr(discord, "BOT_TOKEN", "")
|
|
assert bot.enabled() is False
|
|
monkeypatch.setattr(discord, "BOT_TOKEN", "a-token")
|
|
assert bot.enabled() is True
|