"""Tests for hermes_cli.telegram_managed_bot — QR codes, deep links, pairing.""" from __future__ import annotations from unittest.mock import MagicMock, patch from hermes_cli.telegram_managed_bot import ( DEFAULT_MANAGER_BOT, TELEGRAM_ONBOARDING_URL_ENV, TelegramBotSetupResult, TelegramPairing, create_pairing, generate_bot_username, generate_deep_link, generate_pairing_nonce, poll_for_setup_result, poll_for_token, print_qr_code, render_qr_terminal, ) VALID_TOKEN = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef" SECOND_VALID_TOKEN = "987654321:abcdefghijklmnopqrstuvwxyzABCDEF" class TestGenerateBotUsername: def test_secure_default_format(self): name = generate_bot_username() assert name.startswith("hermes_") assert name.endswith("_bot") assert len(name) == len("hermes_") + 16 + len("_bot") assert len(name) <= 32 def test_profile_name_not_embedded(self): name = generate_bot_username("work") assert "work" not in name assert name.startswith("hermes_") assert name.endswith("_bot") def test_slug_uses_telegram_safe_base32_chars(self): name = generate_bot_username() slug = name.removeprefix("hermes_").removesuffix("_bot") assert len(slug) == 16 assert set(slug) <= set("abcdefghijklmnopqrstuvwxyz234567") def test_uniqueness(self): names = {generate_bot_username() for _ in range(20)} assert len(names) == 20 class TestGenerateDeepLink: def test_basic_format(self): link = generate_deep_link( manager_bot="TestBot", suggested_username="my_bot", ) assert link == "https://t.me/newbot/TestBot/my_bot" def test_with_name(self): link = generate_deep_link( manager_bot="@TestBot", suggested_username="my_bot", suggested_name="My Agent", ) assert "https://t.me/newbot/TestBot/my_bot?" in link assert "name=My+Agent" in link def test_defaults(self): link = generate_deep_link() assert f"https://t.me/newbot/{DEFAULT_MANAGER_BOT}/" in link assert "hermes_" in link def test_name_url_encoded(self): link = generate_deep_link( manager_bot="Bot", suggested_username="test_bot", suggested_name="Hermes & Friends", ) assert "Hermes+%26+Friends" in link class TestPairingNonce: def test_length(self): nonce = generate_pairing_nonce() assert len(nonce) == 32 def test_hex_chars(self): nonce = generate_pairing_nonce() assert all(c in "0123456789abcdef" for c in nonce) def test_uniqueness(self): nonces = {generate_pairing_nonce() for _ in range(100)} assert len(nonces) == 100 class TestQRCode: def test_render_returns_string(self): result = render_qr_terminal("https://example.com") if result: assert isinstance(result, str) assert len(result) > 10 def test_render_graceful_without_qrcode(self): with patch.dict("sys.modules", {"qrcode": None}): render_qr_terminal("https://example.com") def test_print_qr_code_with_url(self, capsys): print_qr_code("https://t.me/newbot/Bot/test_bot") captured = capsys.readouterr() assert "https://t.me/newbot/Bot/test_bot" in captured.out class TestCreatePairing: def test_success(self): mock_resp = MagicMock() mock_resp.status_code = 201 mock_resp.json.return_value = { "pairing_id": "abcdefghijklmnop", "poll_token": "secret-token", "suggested_username": "hermes_abcdefghijklmnop_bot", "deep_link": "https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot?name=Hermes+Agent", "qr_payload": "https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot?name=Hermes+Agent", "expires_at": "2026-05-18T00:00:00.000Z", } with patch( "hermes_cli.telegram_managed_bot.httpx.post", return_value=mock_resp ) as post: pairing = create_pairing("https://api.example.com", bot_name="Hermes Agent") assert pairing == TelegramPairing( pairing_id="abcdefghijklmnop", poll_token="secret-token", suggested_username="hermes_abcdefghijklmnop_bot", deep_link="https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot?name=Hermes+Agent", qr_payload="https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot?name=Hermes+Agent", expires_at="2026-05-18T00:00:00.000Z", ) post.assert_called_once_with( "https://api.example.com/v1/telegram/pairings", json={"bot_name": "Hermes Agent"}, timeout=10.0, ) def test_failure_status(self): mock_resp = MagicMock() mock_resp.status_code = 500 with patch( "hermes_cli.telegram_managed_bot.httpx.post", return_value=mock_resp ): assert create_pairing("https://api.example.com") is None def test_invalid_payload(self): mock_resp = MagicMock() mock_resp.status_code = 201 mock_resp.json.return_value = {"pairing_id": "missing-poll-token"} with patch( "hermes_cli.telegram_managed_bot.httpx.post", return_value=mock_resp ): assert create_pairing("https://api.example.com") is None def test_uses_env_override(self, monkeypatch): monkeypatch.setenv(TELEGRAM_ONBOARDING_URL_ENV, "https://worker.example") mock_resp = MagicMock() mock_resp.status_code = 500 with patch( "hermes_cli.telegram_managed_bot.httpx.post", return_value=mock_resp ) as post: create_pairing() assert post.call_args.args[0] == "https://worker.example/v1/telegram/pairings" class TestPollForToken: def pairing(self): return TelegramPairing( pairing_id="abcdefghijklmnop", poll_token="secret-token", suggested_username="hermes_abcdefghijklmnop_bot", deep_link="https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot", qr_payload="https://t.me/newbot/HermesSetupBot/hermes_abcdefghijklmnop_bot", ) def test_immediate_success(self): mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = { "bot_username": "hermes_abcdefghijklmnop_bot", "owner_user_id": 42, "status": "ready", "token": VALID_TOKEN, } with patch( "hermes_cli.telegram_managed_bot.httpx.get", return_value=mock_resp ) as get: with patch("hermes_cli.telegram_managed_bot.time.sleep"): token = poll_for_token( "https://api.example.com", self.pairing(), timeout=5 ) assert token == VALID_TOKEN assert ( get.call_args.args[0] == "https://api.example.com/v1/telegram/pairings/abcdefghijklmnop" ) assert get.call_args.kwargs["headers"] == { "Authorization": "Bearer secret-token" } def test_setup_result_includes_owner_user_id(self): mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = { "bot_username": "hermes_abcdefghijklmnop_bot", "owner_user_id": 42, "status": "ready", "token": VALID_TOKEN, } with patch("hermes_cli.telegram_managed_bot.httpx.get", return_value=mock_resp): with patch("hermes_cli.telegram_managed_bot.time.sleep"): result = poll_for_setup_result( "https://api.example.com", self.pairing(), timeout=5 ) assert result == TelegramBotSetupResult( token=VALID_TOKEN, bot_username="hermes_abcdefghijklmnop_bot", owner_user_id=42, ) def test_setup_result_accepts_string_owner_user_id(self): mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = { "bot_username": "hermes_abcdefghijklmnop_bot", "owner_user_id": "42", "status": "ready", "token": VALID_TOKEN, } with patch("hermes_cli.telegram_managed_bot.httpx.get", return_value=mock_resp): result = poll_for_setup_result( "https://api.example.com", self.pairing(), timeout=5 ) assert result == TelegramBotSetupResult( token=VALID_TOKEN, bot_username="hermes_abcdefghijklmnop_bot", owner_user_id=42, ) def test_invalid_ready_token_returns_none(self): mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = { "bot_username": "hermes_abcdefghijklmnop_bot", "owner_user_id": 42, "status": "ready", "token": "not-a-real-token", } with patch("hermes_cli.telegram_managed_bot.httpx.get", return_value=mock_resp): with patch("hermes_cli.telegram_managed_bot.time.sleep"): with patch( "hermes_cli.telegram_managed_bot.time.monotonic" ) as mock_time: mock_time.side_effect = [0, 0, 999] assert ( poll_for_token( "https://api.example.com", self.pairing(), timeout=1 ) is None ) def test_timeout_returns_none(self): mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {"status": "waiting"} with patch("hermes_cli.telegram_managed_bot.httpx.get", return_value=mock_resp): with patch("hermes_cli.telegram_managed_bot.time.sleep"): with patch( "hermes_cli.telegram_managed_bot.time.monotonic" ) as mock_time: mock_time.side_effect = [0, 0, 999] token = poll_for_token( "https://api.example.com", self.pairing(), timeout=1 ) assert token is None def test_eventual_success(self): not_ready = MagicMock() not_ready.status_code = 200 not_ready.json.return_value = {"status": "waiting"} ready = MagicMock() ready.status_code = 200 ready.json.return_value = {"status": "ready", "token": SECOND_VALID_TOKEN} call_count = 0 def fake_get(*args, **kwargs): nonlocal call_count call_count += 1 if call_count < 3: return not_ready return ready with patch("hermes_cli.telegram_managed_bot.httpx.get", side_effect=fake_get): with patch("hermes_cli.telegram_managed_bot.time.sleep"): token = poll_for_token( "https://api.example.com", self.pairing(), timeout=30 ) assert token == SECOND_VALID_TOKEN class TestSetupTelegramAuto: def test_setup_helper_exists(self): from hermes_cli.setup import _setup_telegram_auto assert callable(_setup_telegram_auto)