- Удалён dead code except KeyboardInterrupt (никогда не срабатывал в bot.run()) - Добавлен _signal_handler() для SIGINT/SIGTERM с остановкой scheduler и bot.close() - Заменён bot.run(token) на asyncio.run(main()) с async with self.bot: - Добавлен reconnect=True для устойчивости к разрывам gateway - Тесты: string-matching заменены на поведенческие mock-тесты (5 тестов, все OK) - Добавлен CODE_REVIEW.md с результатами ревью
96 lines
4.0 KiB
Python
96 lines
4.0 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):
|
||
"""Проверка, что бот создан с правильным префиксом команд."""
|
||
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):
|
||
"""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):
|
||
"""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_signal_handlers_installed(self):
|
||
"""BotRunner.run() устанавливает обработчики SIGINT и SIGTERM."""
|
||
import asyncio
|
||
import bot
|
||
import signal
|
||
|
||
runner = bot.BotRunner()
|
||
|
||
def fake_run(coro):
|
||
"""Поддельный asyncio.run, который утилизирует корутину."""
|
||
try:
|
||
coro.close()
|
||
except RuntimeError:
|
||
pass # корутина уже закрыта
|
||
|
||
with patch("signal.signal") as mock_signal:
|
||
with patch("asyncio.run", side_effect=fake_run):
|
||
runner.run("fake_token")
|
||
# signal.signal вызван для SIGINT и SIGTERM
|
||
call_args = [call[0][0] for call in mock_signal.call_args_list]
|
||
assert signal.SIGINT in call_args
|
||
assert signal.SIGTERM in call_args
|
||
|
||
def test_code_uses_async_bot_pattern(self):
|
||
"""Проверка, что 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) — это антипаттерн"
|