discordBot/tests/test_bot.py
deadzilla cc2808fb40 fix: graceful shutdown — заменить bot.run() на async with + signal handlers (C2)
- Удалён 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 с результатами ревью
2026-07-07 20:51:32 +05:00

96 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тесты для 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) — это антипаттерн"