"""Интеграционные тесты — проверка взаимодействия компонентов без внешних API. Тестируют реальные объекты (без моков на уровне команд), но мокают только сетевые вызовы. """ import pytest import requests from unittest.mock import AsyncMock, MagicMock, patch @pytest.fixture async def loaded_bot(): """Загрузить все ког-модули в бота.""" import discord from discord.ext import commands intents = discord.Intents.default() intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) from commands import ALL_COMMANDS for cog_class in ALL_COMMANDS: await bot.add_cog(cog_class()) return bot class TestCogLoading: """Проверка загрузки ког-модулей.""" @pytest.mark.asyncio async def test_all_cogs_load(self, loaded_bot) -> None: """Все ког-модули должны загружаться без ошибок.""" from commands import ALL_COMMANDS assert len(loaded_bot.cogs) == len(ALL_COMMANDS) @pytest.mark.asyncio async def test_commands_registered(self, loaded_bot) -> None: """Все команды должны быть зарегистрированы.""" command_names = {cmd.name for cmd in loaded_bot.commands if cmd.cog is not None} expected = {"pg", "nw", "morning", "cat", "hp", "stats", "status"} assert command_names == expected class TestCommandFlow: """Проверка полного потока команд (без сетевых вызовов).""" @pytest.mark.asyncio async def test_cat_command_success(self, loaded_bot) -> None: """Команда !cat должна отправить embed с котиком при успешном ответе API.""" with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock_fetch: mock_fetch.return_value = "https://example.com/cat.jpg" mock_ctx = MagicMock() mock_ctx.author.name = "TestUser" mock_ctx.send = AsyncMock(return_value=None) cat_cmd = loaded_bot.get_command("cat") assert cat_cmd is not None await cat_cmd.callback(cat_cmd.cog, mock_ctx) mock_ctx.send.assert_awaited_once() embed = mock_ctx.send.call_args[1]["embed"] assert embed.title == "Котик для тебя!" assert embed.image.url == "https://example.com/cat.jpg" @pytest.mark.asyncio async def test_cat_command_failure(self, loaded_bot) -> None: """Команда !cat при ошибке API должна отправить fallback сообщение.""" with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock_fetch: mock_fetch.return_value = None mock_ctx = MagicMock() mock_ctx.author.name = "TestUser" mock_ctx.send = AsyncMock(return_value=None) cat_cmd = loaded_bot.get_command("cat") await cat_cmd.callback(cat_cmd.cog, mock_ctx) mock_ctx.send.assert_awaited_once() content = mock_ctx.send.call_args[0][0] assert "Не удалось получить котика" in content @pytest.mark.asyncio async def test_stats_command_flow(self) -> None: """Команда !stats должна показать реальную статистику бота.""" import discord from discord.ext import commands intents = discord.Intents.default() intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) from commands.stats import Stats await bot.add_cog(Stats()) mock_ctx = MagicMock() guild = MagicMock() guild.channels = [] # нет CategoryChannel guild.member_count = 100 mock_ctx.bot = MagicMock() mock_ctx.bot.guilds = [guild] mock_ctx.bot.latency = 0.050 mock_ctx.send = AsyncMock(return_value=None) stats_cmd = bot.get_command("stats") await stats_cmd.callback(stats_cmd.cog, mock_ctx) mock_ctx.send.assert_awaited_once() embed = mock_ctx.send.call_args[1]["embed"] fields = {f.name: f.value for f in embed.fields} assert fields["Серверов"] == "1" assert fields["Каналов"] == "0" assert fields["Пользователей"] == "100" assert fields["Пинг"] == "50.0 мс" class TestUtilityFunctions: """Проверка утилит без моков.""" def test_wmo_codes_mapping(self) -> None: """wmo_to_russian должен переводить известные коды.""" from utils.pogoda import wmo_to_russian assert wmo_to_russian(0) == "Ясно" assert wmo_to_russian(3) == "Пасмурно" assert wmo_to_russian(51) == "Лёгкая морось" assert wmo_to_russian(None) == "Неизвестно" def test_pressure_conversion(self) -> None: """pressure_to_mmhg должен конвертировать mb в мм рт. ст.""" from utils.pogoda import pressure_to_mmhg assert pressure_to_mmhg(1013) == 759.8 # 1013 * 0.750062 = 759.81 assert pressure_to_mmhg("—") == "—" assert pressure_to_mmhg(None) == "—" def test_title_truncation(self) -> None: """truncate_title должен обрезать длинные заголовки.""" from utils.news import truncate_title short = "Короткий заголовок" assert truncate_title(short) == short long_title = "A" * 100 assert len(truncate_title(long_title, 20)) == 23 # 17 + "..." def test_embed_text_truncation(self) -> None: """truncate_embed_text должен обрезать до 4096 символов.""" from utils.news import truncate_embed_text, truncate_embed_field long_text = "A" * 5000 assert len(truncate_embed_text(long_text)) == 4096 assert truncate_embed_text(long_text).endswith("...") assert len(truncate_embed_field(long_text)) == 1024