import asyncio import pytest from types import SimpleNamespace from services import telegram_bot_service pytestmark = pytest.mark.skipif( not telegram_bot_service.TELEGRAM_AVAILABLE, reason="python-telegram-bot 未安裝", ) def _make_polling_update(query): """建立最小的 polling callback 更新結構。""" return SimpleNamespace(callback_query=query) class _FakeMessage: def __init__(self, chat_id=-200, message_id=1): self.chat_id = chat_id self.message_id = message_id self.replies = [] self.edits = [] async def reply_text(self, text, **kwargs): self.replies.append((text, kwargs)) async def reply_chat_action(self, action=None, **kwargs): self.edits.append(("typing", action, kwargs)) class _FakeQuery: def __init__(self, query_id, data, message): self.id = query_id self.data = data self.message = message self.answers = 0 async def answer(self): self.answers += 1 async def edit_message_text(self, *args, **kwargs): return {"ok": True} def _run(coro): return asyncio.run(coro) def test_default_category_keyboard_uses_handled_trend_prefix(): from services.telegram_bot_service import CATEGORIES, TrendTelegramBot bot = TrendTelegramBot(token="dummy") keyboard = bot._get_category_keyboard() callback_values = [ button.callback_data for row in keyboard.inline_keyboard for button in row ] assert f"trend_{CATEGORIES[0]}" in callback_values assert not any(value.startswith("cat_") for value in callback_values) def test_polling_callback_dedup_without_update_id(monkeypatch): from services.telegram_bot_service import TrendTelegramBot seen = {} def fake_dedupe(_key, namespace="telegram_update"): if _key in seen: return True seen[_key] = True return False bot = TrendTelegramBot(token="dummy") called = [] async def fake_openclaw_callback(*args): called.append(args) monkeypatch.setattr(telegram_bot_service, "is_global_duplicate_update", fake_dedupe) bot._handle_openclaw_callback = fake_openclaw_callback context = SimpleNamespace(user_data={}) query = _FakeQuery("cb-no-id", "cmd:sales:2026/04/30", _FakeMessage(message_id=123)) _run(bot.handle_callback(_make_polling_update(query), context)) _run(bot.handle_callback(_make_polling_update(query), context)) assert len(called) == 1 assert called[0][2] == "cmd:sales:2026/04/30" def test_polling_callback_dedup_depends_on_message_id(monkeypatch): from services.telegram_bot_service import TrendTelegramBot seen = {} def fake_dedupe(_key, namespace="telegram_update"): if _key in seen: return True seen[_key] = True return False bot = TrendTelegramBot(token="dummy") called = [] async def fake_openclaw_callback(*args): called.append(args) monkeypatch.setattr(telegram_bot_service, "is_global_duplicate_update", fake_dedupe) bot._handle_openclaw_callback = fake_openclaw_callback context = SimpleNamespace(user_data={}) q1 = _FakeQuery("cb-no-id", "cmd:sales:2026/04/30", _FakeMessage(message_id=201)) q2 = _FakeQuery("cb-no-id", "cmd:sales:2026/04/30", _FakeMessage(message_id=202)) _run(bot.handle_callback(_make_polling_update(q1), context)) _run(bot.handle_callback(_make_polling_update(q2), context)) assert called == [ (q1, context, "cmd:sales:2026/04/30"), (q2, context, "cmd:sales:2026/04/30"), ] def test_polling_callback_dedup_with_same_query_id_different_update_id(monkeypatch): from services.telegram_bot_service import TrendTelegramBot seen = {} def fake_dedupe(_key, namespace="telegram_update"): if _key in seen: return True seen[_key] = True return False bot = TrendTelegramBot(token="dummy") called = [] async def fake_openclaw_callback(*args): called.append(args) monkeypatch.setattr(telegram_bot_service, "is_global_duplicate_update", fake_dedupe) bot._handle_openclaw_callback = fake_openclaw_callback context = SimpleNamespace(user_data={}) q1 = _FakeQuery("cb-repeat", "cmd:sales:2026/04/30", _FakeMessage(message_id=301)) q2 = _FakeQuery("cb-repeat", "cmd:sales:2026/04/30", _FakeMessage(message_id=301)) # 為了模擬 update_id 不同,帶入不同的 update 物件 u1 = SimpleNamespace(callback_query=q1, effective_user=SimpleNamespace(id=777), update_id=30001) u2 = SimpleNamespace(callback_query=q2, effective_user=SimpleNamespace(id=777), update_id=30002) _run(bot.handle_callback(u1, context)) _run(bot.handle_callback(u2, context)) assert called == [(q1, context, "cmd:sales:2026/04/30")] def test_polling_callback_normalizes_legacy_menu_prefix(monkeypatch): from services.telegram_bot_service import TrendTelegramBot seen = {} def fake_dedupe(_key, namespace="telegram_update"): if _key in seen: return True seen[_key] = True return False bot = TrendTelegramBot(token="dummy") normalized = [] async def fake_openclaw_callback(query, context, data): normalized.append(data) monkeypatch.setattr(telegram_bot_service, "is_global_duplicate_update", fake_dedupe) bot._handle_openclaw_callback = fake_openclaw_callback context = SimpleNamespace(user_data={}) query = _FakeQuery("cb-menu", "menu_main", _FakeMessage(message_id=321)) _run(bot.handle_callback(_make_polling_update(query), context)) assert normalized == ["menu:main"] def test_polling_cmd_callback_uses_callback_context(monkeypatch): from services.telegram_bot_service import TrendTelegramBot from routes import openclaw_bot_routes as openclaw seen = {} def fake_dedupe(_key, namespace="telegram_update"): if _key in seen: return True seen[_key] = True return False called = [] def fake_handle_cmd(cmd, arg, chat_id, reply_to): called.append((cmd, arg, chat_id, reply_to, openclaw._CMD_FROM_CALLBACK_CTX.get())) return None bot = TrendTelegramBot(token="dummy") context = SimpleNamespace(user_data={}) query = _FakeQuery("cb-context", "cmd:sales:2026/04/30", _FakeMessage(message_id=401)) monkeypatch.setattr(telegram_bot_service, "is_global_duplicate_update", fake_dedupe) monkeypatch.setattr(openclaw, "handle_cmd", fake_handle_cmd) _run(bot.handle_callback(_make_polling_update(query), context)) assert called == [("sales", "2026/04/30", -200, 401, True)]