185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
"""
|
||
Тесты для 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)
|