fix(BATON-007): add validate_bot_token() for startup detection and fix test mocks
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
cbc15eeedc
commit
a2b38ef815
4 changed files with 75 additions and 1 deletions
|
|
@ -71,6 +71,11 @@ async def lifespan(app: FastAPI):
|
||||||
await db.init_db()
|
await db.init_db()
|
||||||
logger.info("Database initialized")
|
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:
|
if config.WEBHOOK_ENABLED:
|
||||||
await telegram.set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET)
|
await telegram.set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET)
|
||||||
logger.info("Webhook registered")
|
logger.info("Webhook registered")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,25 @@ logger = logging.getLogger(__name__)
|
||||||
_TELEGRAM_API = "https://api.telegram.org/bot{token}/{method}"
|
_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:
|
async def send_message(text: str) -> None:
|
||||||
url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="sendMessage")
|
url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="sendMessage")
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
async with httpx.AsyncClient(timeout=10) as client:
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ def make_app_client():
|
||||||
"""
|
"""
|
||||||
tg_set_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/setWebhook"
|
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"
|
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
|
@contextlib.asynccontextmanager
|
||||||
async def _ctx():
|
async def _ctx():
|
||||||
|
|
@ -86,6 +87,9 @@ def make_app_client():
|
||||||
from backend.main import app
|
from backend.main import app
|
||||||
|
|
||||||
mock_router = respx.mock(assert_all_called=False)
|
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(
|
mock_router.post(tg_set_url).mock(
|
||||||
return_value=httpx.Response(200, json={"ok": True, "result": True})
|
return_value=httpx.Response(200, json={"ok": True, "result": True})
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,57 @@ import pytest
|
||||||
import respx
|
import respx
|
||||||
|
|
||||||
from backend import config
|
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"
|
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"
|
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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue