92 lines
4.3 KiB
Python
92 lines
4.3 KiB
Python
"""
|
||
Тесты для bot.py — проверка обработки ошибок запуска бота.
|
||
|
||
Покрывают пункт 1.2 из PLAN_OF_WORKS.md:
|
||
- Graceful shutdown через signal handlers
|
||
- Логирование и обработка исключений (LoginFailure, HTTPException)
|
||
- async with bot паттерн вместо bot.run()
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
# Добавляем корень проекта в путь импорта
|
||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT_DIR))
|
||
|
||
|
||
class TestBotInit:
|
||
"""Тесты для инициализации бота."""
|
||
|
||
def test_bot_created_with_default_prefix(self) -> None:
|
||
"""Проверка, что бот создан с правильным префиксом команд."""
|
||
import bot
|
||
|
||
runner = bot.BotRunner()
|
||
try:
|
||
assert runner.bot.command_prefix == "!", (
|
||
f"Команда должна быть с префиксом '!', а не '{runner.bot.command_prefix}'"
|
||
)
|
||
finally:
|
||
runner.stop_event.set()
|
||
|
||
|
||
class TestBotErrorHandling:
|
||
"""Тесты для проверки обработки ошибок запуска бота."""
|
||
|
||
def test_bot_handles_login_failure(self) -> None:
|
||
"""BotRunner.run() обрабатывает discord.LoginFailure."""
|
||
import bot
|
||
import discord
|
||
|
||
runner = bot.BotRunner()
|
||
with patch.object(runner.bot, "start", side_effect=discord.LoginFailure("bad token")):
|
||
with patch.object(runner.bot, "__aenter__", return_value=runner.bot):
|
||
with patch.object(runner.bot, "__aexit__", return_value=None):
|
||
with patch("sys.exit") as mock_exit:
|
||
runner.run("fake_token")
|
||
mock_exit.assert_called_once_with(1)
|
||
|
||
def test_bot_handles_http_exception(self) -> None:
|
||
"""BotRunner.run() обрабатывает discord.HTTPException."""
|
||
import bot
|
||
import discord
|
||
|
||
runner = bot.BotRunner()
|
||
mock_response = MagicMock(status=502)
|
||
with patch.object(runner.bot, "start", side_effect=discord.HTTPException(mock_response, "Bad Gateway")):
|
||
with patch.object(runner.bot, "__aenter__", return_value=runner.bot):
|
||
with patch.object(runner.bot, "__aexit__", return_value=None):
|
||
with patch("sys.exit") as mock_exit:
|
||
runner.run("fake_token")
|
||
mock_exit.assert_called_once_with(1)
|
||
|
||
def test_shutdown_uses_on_shutdown_listener(self) -> None:
|
||
"""BotRunner.run() регистрирует on_shutdown вместо signal handlers.
|
||
|
||
Signal handlers с asyncio.new_event_loop() создают race condition
|
||
с основным loop. Вместо них используется:
|
||
- discord.py on_shutdown событие для остановки планировщика
|
||
- async with self.bot (context manager) для graceful shutdown
|
||
"""
|
||
import bot
|
||
|
||
runner = bot.BotRunner()
|
||
# Проверяем, что _on_shutdown и _on_shutdown_async методы существуют
|
||
assert hasattr(runner, "_on_shutdown"), "Метод _on_shutdown должен существовать"
|
||
assert hasattr(runner, "_on_shutdown_async"), "Метод _on_shutdown_async должен существовать"
|
||
# Проверяем, что signal модуль НЕ импортирован в bot.py
|
||
with open(ROOT_DIR / "bot.py", encoding="utf-8") as f:
|
||
content = f.read()
|
||
assert "signal.signal" not in content, "Не должно быть signal.signal — используется on_shutdown"
|
||
|
||
def test_code_uses_async_bot_pattern(self) -> None:
|
||
"""Проверка, что bot.py использует async with / asyncio.run."""
|
||
with open(ROOT_DIR / "bot.py", encoding="utf-8") as f:
|
||
content = f.read()
|
||
|
||
assert "async with self.bot" in content, "Должен быть паттерн 'async with self.bot'"
|
||
assert "asyncio.run(main())" in content, "Должен быть вызов asyncio.run()"
|
||
assert "bot.run(token)" not in content, "Не должно быть bot.run(token) — это антипаттерн"
|