""" Тесты для 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_shutdown_uses_on_shutdown_listener(self): """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): """Проверка, что 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) — это антипаттерн"