From 9a690ed421969e2e3515c1ea9c6e8396f7569ec7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 2 Sep 2024 21:23:24 +0200 Subject: [PATCH] Handle telegram polling errors (#124327) --- .../components/telegram_bot/polling.py | 16 ++- .../telegram_bot/test_telegram_bot.py | 103 +++++++++++++++++- 2 files changed, 114 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 45d2ee65b45a4a..bee7f752f6c987 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -25,14 +25,22 @@ async def async_setup_platform(hass, bot, config): async def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" + if context.error: + error_callback(context.error, update) + + +def error_callback(error: Exception, update: Update | None = None) -> None: + """Log the error.""" try: - if context.error: - raise context.error + raise error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass except TelegramError: - _LOGGER.error('Update "%s" caused error: "%s"', update, context.error) + if update is not None: + _LOGGER.error('Update "%s" caused error: "%s"', update, error) + else: + _LOGGER.error("%s: %s", error.__class__.__name__, error) class PollBot(BaseTelegramBotEntity): @@ -53,7 +61,7 @@ async def start_polling(self, event=None): """Start the polling task.""" _LOGGER.debug("Starting polling") await self.application.initialize() - await self.application.updater.start_polling() + await self.application.updater.start_polling(error_callback=error_callback) await self.application.start() async def stop_polling(self, event=None): diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index aad758827ca294..bdf6ba72fccf79 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -1,8 +1,11 @@ """Tests for the telegram_bot component.""" -from unittest.mock import AsyncMock, patch +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch +import pytest from telegram import Update +from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut from homeassistant.components.telegram_bot import ( ATTR_MESSAGE, @@ -11,6 +14,7 @@ SERVICE_SEND_MESSAGE, ) from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component @@ -188,6 +192,103 @@ async def test_polling_platform_message_text_update( assert isinstance(events[0].context, Context) +@pytest.mark.parametrize( + ("error", "log_message"), + [ + ( + TelegramError("Telegram error"), + 'caused error: "Telegram error"', + ), + (NetworkError("Network error"), ""), + (RetryAfter(42), ""), + (TimedOut("TimedOut error"), ""), + ], +) +async def test_polling_platform_add_error_handler( + hass: HomeAssistant, + config_polling: dict[str, Any], + update_message_text: dict[str, Any], + caplog: pytest.LogCaptureFixture, + error: Exception, + log_message: str, +) -> None: + """Test polling add error handler.""" + with patch( + "homeassistant.components.telegram_bot.polling.ApplicationBuilder" + ) as application_builder_class: + await async_setup_component( + hass, + DOMAIN, + config_polling, + ) + await hass.async_block_till_done() + + application = ( + application_builder_class.return_value.bot.return_value.build.return_value + ) + application.updater.stop = AsyncMock() + application.stop = AsyncMock() + application.shutdown = AsyncMock() + process_error = application.add_error_handler.call_args[0][0] + application.bot.defaults.tzinfo = None + update = Update.de_json(update_message_text, application.bot) + + await process_error(update, MagicMock(error=error)) + + assert log_message in caplog.text + + +@pytest.mark.parametrize( + ("error", "log_message"), + [ + ( + TelegramError("Telegram error"), + "TelegramError: Telegram error", + ), + (NetworkError("Network error"), ""), + (RetryAfter(42), ""), + (TimedOut("TimedOut error"), ""), + ], +) +async def test_polling_platform_start_polling_error_callback( + hass: HomeAssistant, + config_polling: dict[str, Any], + caplog: pytest.LogCaptureFixture, + error: Exception, + log_message: str, +) -> None: + """Test polling add error handler.""" + with patch( + "homeassistant.components.telegram_bot.polling.ApplicationBuilder" + ) as application_builder_class: + await async_setup_component( + hass, + DOMAIN, + config_polling, + ) + await hass.async_block_till_done() + + application = ( + application_builder_class.return_value.bot.return_value.build.return_value + ) + application.initialize = AsyncMock() + application.updater.start_polling = AsyncMock() + application.start = AsyncMock() + application.updater.stop = AsyncMock() + application.stop = AsyncMock() + application.shutdown = AsyncMock() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + error_callback = application.updater.start_polling.call_args.kwargs[ + "error_callback" + ] + + error_callback(error) + + assert log_message in caplog.text + + async def test_webhook_endpoint_unauthorized_update_doesnt_generate_telegram_text_event( hass: HomeAssistant, webhook_platform,