diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..b78d68d --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,154 @@ +"""Интеграционные тесты — проверка взаимодействия компонентов без внешних 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