from __future__ import annotations import asyncio import sys from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock from gateway.config import GatewayConfig, Platform, PlatformConfig from gateway.pairing import PairingStore from gateway.session import SessionSource from gateway.user_verification import GatewayUserStore from tests.gateway._plugin_adapter_loader import load_plugin_adapter def _ensure_telegram_mock() -> None: if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"): return telegram_mod = MagicMock() telegram_mod.ext.ContextTypes.DEFAULT_TYPE = type(None) telegram_mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2" telegram_mod.constants.ChatType.GROUP = "group" telegram_mod.constants.ChatType.SUPERGROUP = "supergroup" telegram_mod.constants.ChatType.CHANNEL = "channel" telegram_mod.constants.ChatType.PRIVATE = "private" for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"): sys.modules.setdefault(name, telegram_mod) _ensure_telegram_mock() from gateway.run import GatewayRunner # noqa: E402 from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 _line = load_plugin_adapter("line") LineAdapter = _line.LineAdapter def _make_telegram_adapter() -> TelegramAdapter: adapter = object.__new__(TelegramAdapter) adapter.platform = Platform.TELEGRAM adapter.config = PlatformConfig(enabled=True, token="fake-token", extra={}) adapter._bot = SimpleNamespace(id=999, username="test_bot") adapter._message_handler = AsyncMock() adapter._pending_text_batches = {} adapter._pending_text_batch_tasks = {} adapter._text_batch_delay_seconds = 0.01 adapter._text_batch_split_delay_seconds = 0.01 adapter._mention_patterns = adapter._compile_mention_patterns() adapter._forum_lock = asyncio.Lock() adapter._forum_command_registered = set() adapter._active_sessions = {} adapter._pending_messages = {} return adapter def _make_telegram_message(text: str, *, from_user_id: int, chat_id: int, chat_type: str = "private"): return SimpleNamespace( message_id=42, text=text, caption=None, entities=[], caption_entities=[], message_thread_id=None, is_topic_message=False, chat=SimpleNamespace(id=chat_id, type=chat_type, title="Smoke", is_forum=False), from_user=SimpleNamespace(id=from_user_id, full_name="Smoke User", first_name="Smoke"), reply_to_message=None, date=None, location=None, photo=None, video=None, audio=None, voice=None, document=None, sticker=None, media_group_id=None, ) def _make_runner(platform: Platform) -> GatewayRunner: runner = object.__new__(GatewayRunner) runner.config = GatewayConfig(platforms={platform: PlatformConfig(enabled=True)}) runner.adapters = {platform: SimpleNamespace()} runner.pairing_store = PairingStore() return runner async def _run_telegram_text(adapter: TelegramAdapter, update) -> None: await adapter._handle_text_message(update, SimpleNamespace()) pending = list(adapter._pending_text_batch_tasks.values()) if pending: await asyncio.gather(*pending) def test_line_dm_verification_smoke(monkeypatch): for key in ( "LINE_ALLOWED_USERS", "LINE_ALLOWED_GROUPS", "LINE_ALLOWED_ROOMS", "LINE_ALLOW_ALL_USERS", "GATEWAY_ALLOWED_USERS", "GATEWAY_ALLOW_ALL_USERS", ): monkeypatch.delenv(key, raising=False) store = GatewayUserStore() adapter = LineAdapter( PlatformConfig( enabled=True, extra={"channel_access_token": "tok", "channel_secret": "sec"}, ) ) adapter._client = MagicMock() adapter._client.reply = AsyncMock() adapter._client.push = AsyncMock( side_effect=RuntimeError('LINE push 429: {"message":"You have reached your monthly limit."}') ) adapter._client.loading = AsyncMock() sent = {} async def _fake_email_send(_cfg, chat_id, message, **kwargs): sent["chat_id"] = chat_id sent["message"] = message sent["kwargs"] = kwargs return {"success": True} monkeypatch.setattr(_line, "_email_standalone_send", _fake_email_send) decisions = [] async def _handle_message(event): decisions.append( store.process_inbound_message( event.source, "line-smoke@bremen.com.tw", public_base_url="https://agent.example.com", ) ) adapter.handle_message = _handle_message event = { "type": "message", "replyToken": "rt-1", "source": {"type": "user", "userId": "UlineSmoke"}, "message": {"id": "m1", "type": "text", "text": "line-smoke@bremen.com.tw"}, } asyncio.run(adapter._dispatch_event(event)) assert len(decisions) == 1 assert decisions[0].action == "send_verification_email" assert decisions[0].token source = SessionSource( platform=Platform("line"), user_id="UlineSmoke", chat_id="UlineSmoke", user_name="Smoke User", chat_type="dm", ) assert store.is_verified_source(source) is False verify = store.verify_token(decisions[0].token) assert verify.success is True assert store.is_verified_source(source) is True assert store.get_verified_email_for_source(source) == "line-smoke@bremen.com.tw" payload = asyncio.run(adapter._send_email_quota_fallback("UlineSmoke", "hello", mode="final")) assert payload is not None assert payload["email"] == "line-smoke@bremen.com.tw" assert sent["chat_id"] == "line-smoke@bremen.com.tw" store.unbind_identity("line", "UlineSmoke") assert store.is_verified_source(source) is False assert store.get_verified_email_for_source(source) is None payload_after = asyncio.run(adapter._send_email_quota_fallback("UlineSmoke", "hello", mode="final")) assert payload_after is None with store._connect() as conn: conn.execute( "UPDATE verification_requests SET last_sent_at = datetime('now', '-11 minutes') WHERE platform = ? AND external_user_id = ?", ("line", "UlineSmoke"), ) conn.commit() decisions.clear() asyncio.run(adapter._dispatch_event(event)) assert len(decisions) == 1 assert decisions[0].action == "send_verification_email" def test_telegram_dm_verification_smoke(monkeypatch): for key in ( "TELEGRAM_ALLOWED_USERS", "TELEGRAM_GROUP_ALLOWED_USERS", "TELEGRAM_GROUP_ALLOWED_CHATS", "TELEGRAM_ALLOW_ALL_USERS", "GATEWAY_ALLOWED_USERS", "GATEWAY_ALLOW_ALL_USERS", ): monkeypatch.delenv(key, raising=False) store = GatewayUserStore() adapter = _make_telegram_adapter() decisions = [] async def _handle_message(event): decisions.append( store.process_inbound_message( event.source, "tg-smoke@journeys.com.tw", public_base_url="https://agent.example.com", ) ) adapter.handle_message = _handle_message update = SimpleNamespace( update_id=1, message=_make_telegram_message( "tg-smoke@journeys.com.tw", from_user_id=601504103, chat_id=601504103, chat_type="private", ), effective_message=None, ) asyncio.run(_run_telegram_text(adapter, update)) assert len(decisions) == 1 assert decisions[0].action == "send_verification_email" assert decisions[0].token source = SessionSource( platform=Platform.TELEGRAM, user_id="601504103", chat_id="601504103", user_name="Smoke User", chat_type="dm", ) assert store.is_verified_source(source) is False verify = store.verify_token(decisions[0].token) assert verify.success is True assert store.is_verified_source(source) is True assert store.get_verified_email_for_source(source) == "tg-smoke@journeys.com.tw" runner = _make_runner(Platform.TELEGRAM) assert runner._is_user_authorized(source) is False PairingStore()._approve_user("telegram", "601504103", "Smoke User") assert runner._is_user_authorized(source) is True store.unbind_identity("telegram", "601504103") assert store.is_verified_source(source) is False assert store.get_verified_email_for_source(source) is None assert runner._is_user_authorized(source) is False with store._connect() as conn: conn.execute( "UPDATE verification_requests SET last_sent_at = datetime('now', '-11 minutes') WHERE platform = ? AND external_user_id = ?", ("telegram", "601504103"), ) conn.commit() decisions.clear() asyncio.run(_run_telegram_text(adapter, update)) assert len(decisions) == 1 assert decisions[0].action == "send_verification_email"