157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
"""Интеграционные тесты — проверка взаимодействия компонентов без внешних API.
|
||
|
||
Тестируют реальные объекты (без моков на уровне команд),
|
||
но мокают только сетевые вызовы.
|
||
"""
|
||
|
||
import pytest
|
||
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
|