""" Тесты для helper_bot.main: start_bot_with_retry, start_bot. """ import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from helper_bot.main import start_bot, start_bot_with_retry @pytest.mark.unit @pytest.mark.asyncio class TestStartBotWithRetry: """Тесты для start_bot_with_retry.""" async def test_success_on_first_try_exits_immediately( self, mock_bot, mock_dispatcher ): """При успешном start_polling с первой попытки цикл завершается без повторов.""" mock_dispatcher.start_polling = AsyncMock() await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3) mock_dispatcher.start_polling.assert_awaited_once_with( mock_bot, skip_updates=True ) @patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock) async def test_network_error_retries_then_succeeds( self, mock_sleep, mock_bot, mock_dispatcher ): """При сетевой ошибке выполняется повтор с задержкой, затем успех.""" mock_dispatcher.start_polling = AsyncMock( side_effect=[ConnectionError("connection reset"), None] ) await start_bot_with_retry( mock_bot, mock_dispatcher, max_retries=3, base_delay=0.1 ) assert mock_dispatcher.start_polling.await_count == 2 mock_sleep.assert_awaited_once() # base_delay * (2 ** 0) = 0.1 mock_sleep.assert_awaited_with(0.1) async def test_non_network_error_raises_immediately( self, mock_bot, mock_dispatcher ): """При не-сетевой ошибке исключение пробрасывается без повторов.""" mock_dispatcher.start_polling = AsyncMock(side_effect=ValueError("critical")) with pytest.raises(ValueError, match="critical"): await start_bot_with_retry(mock_bot, mock_dispatcher, max_retries=3) mock_dispatcher.start_polling.assert_awaited_once() @patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock) async def test_max_retries_exceeded_raises( self, mock_sleep, mock_bot, mock_dispatcher ): """При исчерпании попыток из-за сетевых ошибок исключение пробрасывается.""" mock_dispatcher.start_polling = AsyncMock( side_effect=ConnectionError("network error") ) with pytest.raises(ConnectionError, match="network error"): await start_bot_with_retry( mock_bot, mock_dispatcher, max_retries=2, base_delay=0.01 ) assert mock_dispatcher.start_polling.await_count == 2 assert mock_sleep.await_count == 1 async def test_timeout_error_triggers_retry(self, mock_bot, mock_dispatcher): """Ошибка с 'timeout' в сообщении считается сетевой и даёт повтор.""" call_count = 0 async def polling(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: raise TimeoutError("timeout while connecting") return None mock_dispatcher.start_polling = AsyncMock(side_effect=polling) with patch("helper_bot.main.asyncio.sleep", new_callable=AsyncMock): await start_bot_with_retry( mock_bot, mock_dispatcher, max_retries=3, base_delay=0.01 ) assert call_count == 2 @pytest.mark.unit @pytest.mark.asyncio class TestStartBot: """Тесты для start_bot с моками Bot, Dispatcher, start_metrics_server и т.д.""" @pytest.fixture def mock_bdf(self, test_settings): """Мок фабрики зависимостей (bdf) с настройками и scoring_manager.""" bdf = MagicMock() bdf.settings = { **test_settings, "Metrics": {"host": "127.0.0.1", "port": 9090}, } scoring_manager = MagicMock() scoring_manager.close = AsyncMock() bdf.get_scoring_manager = MagicMock(return_value=scoring_manager) return bdf @patch("helper_bot.main.stop_metrics_server", new_callable=AsyncMock) @patch("helper_bot.main.start_bot_with_retry", new_callable=AsyncMock) @patch("helper_bot.main.start_metrics_server", new_callable=AsyncMock) @patch("helper_bot.main.VoiceHandlers") @patch("helper_bot.main.Dispatcher") @patch("helper_bot.main.Bot") async def test_start_bot_calls_metrics_server_and_polling( self, mock_bot_cls, mock_dp_cls, mock_voice_handlers_cls, mock_start_metrics, mock_start_retry, mock_stop_metrics, mock_bdf, ): """start_bot создаёт Bot и Dispatcher, запускает метрики, delete_webhook, start_bot_with_retry; в finally — stop_metrics и закрытие ресурсов.""" mock_bot = MagicMock() mock_bot.delete_webhook = AsyncMock() mock_bot.session = MagicMock() mock_bot.session.close = AsyncMock() mock_bot_cls.return_value = mock_bot mock_dp = MagicMock() mock_dp.update = MagicMock() mock_dp.update.outer_middleware = MagicMock(return_value=None) mock_dp.include_routers = MagicMock() mock_dp.shutdown = MagicMock(return_value=lambda f: None) mock_dp_cls.return_value = mock_dp mock_voice_router = MagicMock() mock_voice_handlers_cls.return_value.router = mock_voice_router result = await start_bot(mock_bdf) mock_bot_cls.assert_called_once() mock_dp_cls.assert_called_once() mock_bot.delete_webhook.assert_awaited_once_with(drop_pending_updates=True) mock_start_metrics.assert_awaited_once_with("127.0.0.1", 9090) mock_start_retry.assert_awaited_once() mock_stop_metrics.assert_awaited_once() mock_bdf.get_scoring_manager.return_value.close.assert_awaited() mock_bot.session.close.assert_awaited() assert result is mock_bot @patch("helper_bot.main.stop_metrics_server", new_callable=AsyncMock) @patch("helper_bot.main.start_bot_with_retry", new_callable=AsyncMock) @patch("helper_bot.main.start_metrics_server", new_callable=AsyncMock) @patch("helper_bot.main.VoiceHandlers") @patch("helper_bot.main.Dispatcher") @patch("helper_bot.main.Bot") async def test_start_bot_uses_default_metrics_host_port_when_not_in_settings( self, mock_bot_cls, mock_dp_cls, mock_voice_handlers_cls, mock_start_metrics, mock_start_retry, mock_stop_metrics, mock_bdf, test_settings, ): """Если в настройках нет Metrics, используются host 0.0.0.0 и port 8080.""" mock_bdf.settings = test_settings mock_bot = MagicMock() mock_bot.delete_webhook = AsyncMock() mock_bot.session = MagicMock() mock_bot.session.close = AsyncMock() mock_bot_cls.return_value = mock_bot mock_dp = MagicMock() mock_dp.update = MagicMock() mock_dp.update.outer_middleware = MagicMock(return_value=None) mock_dp.include_routers = MagicMock() mock_dp.shutdown = MagicMock(return_value=lambda f: None) mock_dp_cls.return_value = mock_dp mock_voice_handlers_cls.return_value.router = MagicMock() await start_bot(mock_bdf) mock_start_metrics.assert_awaited_once_with("0.0.0.0", 8080)