From a2b38ef8152271beb92c51a7f19fde1655be3511 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Sat, 21 Mar 2026 08:54:07 +0200 Subject: [PATCH] fix(BATON-007): add validate_bot_token() for startup detection and fix test mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validate_bot_token() to backend/telegram.py: calls getMe on startup, logs ERROR if token is invalid (never raises per #1215 contract) - Call validate_bot_token() in lifespan() after db.init_db() for early detection - Update conftest.py make_app_client() to mock getMe endpoint - Add 3 tests for validate_bot_token (200, 401, network error cases) Root cause: CHAT_ID=5190015988 (positive) was wrong — fixed to -5190015988 on server per decision #1212. Group "Big Red Button" confirmed via getChat. Service restarted. Co-Authored-By: Claude Sonnet 4.6 --- backend/main.py | 5 +++++ backend/telegram.py | 19 +++++++++++++++++ tests/conftest.py | 4 ++++ tests/test_telegram.py | 48 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index d2c275c..7fb9d19 100644 --- a/backend/main.py +++ b/backend/main.py @@ -71,6 +71,11 @@ async def lifespan(app: FastAPI): await db.init_db() logger.info("Database initialized") + if not await telegram.validate_bot_token(): + logger.error( + "CRITICAL: BOT_TOKEN is invalid — Telegram delivery is broken. Update .env and restart." + ) + if config.WEBHOOK_ENABLED: await telegram.set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET) logger.info("Webhook registered") diff --git a/backend/telegram.py b/backend/telegram.py index b7018e9..0633462 100644 --- a/backend/telegram.py +++ b/backend/telegram.py @@ -14,6 +14,25 @@ logger = logging.getLogger(__name__) _TELEGRAM_API = "https://api.telegram.org/bot{token}/{method}" +async def validate_bot_token() -> bool: + """Validate BOT_TOKEN by calling getMe. Logs ERROR if invalid. Never raises.""" + url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="getMe") + async with httpx.AsyncClient(timeout=10) as client: + try: + resp = await client.get(url) + if resp.status_code == 200: + bot_name = resp.json().get("result", {}).get("username", "?") + logger.info("Telegram token valid, bot: @%s", bot_name) + return True + logger.error( + "BOT_TOKEN invalid — getMe returned %s: %s", resp.status_code, resp.text + ) + return False + except Exception as exc: + logger.error("BOT_TOKEN validation failed (network): %s", exc) + return False + + async def send_message(text: str) -> None: url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="sendMessage") async with httpx.AsyncClient(timeout=10) as client: diff --git a/tests/conftest.py b/tests/conftest.py index 24b0ff3..0801e32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,6 +79,7 @@ def make_app_client(): """ tg_set_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/setWebhook" send_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage" + get_me_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/getMe" @contextlib.asynccontextmanager async def _ctx(): @@ -86,6 +87,9 @@ def make_app_client(): from backend.main import app mock_router = respx.mock(assert_all_called=False) + mock_router.get(get_me_url).mock( + return_value=httpx.Response(200, json={"ok": True, "result": {"username": "testbot"}}) + ) mock_router.post(tg_set_url).mock( return_value=httpx.Response(200, json={"ok": True, "result": True}) ) diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 264625b..c55a6a0 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -34,11 +34,57 @@ import pytest import respx from backend import config -from backend.telegram import SignalAggregator, send_message, set_webhook +from backend.telegram import SignalAggregator, send_message, set_webhook, validate_bot_token SEND_URL = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage" WEBHOOK_URL_API = f"https://api.telegram.org/bot{config.BOT_TOKEN}/setWebhook" +GET_ME_URL = f"https://api.telegram.org/bot{config.BOT_TOKEN}/getMe" + + +# --------------------------------------------------------------------------- +# validate_bot_token +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_validate_bot_token_returns_true_on_200(): + """validate_bot_token returns True when getMe responds 200.""" + with respx.mock(assert_all_called=False) as mock: + mock.get(GET_ME_URL).mock( + return_value=httpx.Response(200, json={"ok": True, "result": {"username": "batonbot"}}) + ) + result = await validate_bot_token() + assert result is True + + +@pytest.mark.asyncio +async def test_validate_bot_token_returns_false_on_401(caplog): + """validate_bot_token returns False and logs ERROR when getMe responds 401.""" + import logging + + with respx.mock(assert_all_called=False) as mock: + mock.get(GET_ME_URL).mock( + return_value=httpx.Response(401, json={"ok": False, "description": "Unauthorized"}) + ) + with caplog.at_level(logging.ERROR, logger="backend.telegram"): + result = await validate_bot_token() + + assert result is False + assert any("401" in record.message for record in caplog.records) + + +@pytest.mark.asyncio +async def test_validate_bot_token_returns_false_on_network_error(caplog): + """validate_bot_token returns False and logs ERROR on network failure — never raises.""" + import logging + + with respx.mock(assert_all_called=False) as mock: + mock.get(GET_ME_URL).mock(side_effect=httpx.ConnectError("connection refused")) + with caplog.at_level(logging.ERROR, logger="backend.telegram"): + result = await validate_bot_token() + + assert result is False + assert len(caplog.records) >= 1 # ---------------------------------------------------------------------------