From bc843abc63e0e0b53699dd08b187dc966355c642 Mon Sep 17 00:00:00 2001 From: deadzilla Date: Thu, 9 Jul 2026 13:07:09 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20Cat,=20News,=20Morning=20=D0=BA=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=D0=B2,=20TextHelpCommand=20=D0=B8=20truncate-=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B9=20(282=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0,=20=D0=B2=D1=81=D0=B5=20=D0=BF=D1=80=D0=BE=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D1=8F=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 ++- tests/test_commands_cat.py | 136 +++++++++++++++++++++ tests/test_commands_morning.py | 84 +++++++++++++ tests/test_commands_news.py | 151 +++++++++++++++++++++++ tests/test_format_articles.py | 99 ++++++++++++++- tests/test_help_command.py | 216 +++++++++++++++++++++++++++++++++ 6 files changed, 696 insertions(+), 4 deletions(-) create mode 100644 tests/test_commands_cat.py create mode 100644 tests/test_commands_morning.py create mode 100644 tests/test_commands_news.py create mode 100644 tests/test_help_command.py diff --git a/README.md b/README.md index 90f9576..2603faa 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,12 @@ tests/ # pytest-тесты test_fetch_weather.py # fetch_weather, fetch_open_meteo test_format_articles.py # truncate_title, _parse_date, format_articles test_commands_pg.py # Pg cog - test_bot.py # инициализация бота + test_commands_cat.py # Cat cog, команда !cat + test_commands_news.py # News cog, команда !nw + test_commands_morning.py # Morning cog, команда !morning + test_bot.py # инициализация бота, обработка ошибок запуска test_morning_runner.py# тесты morning runner-а + test_help_command.py # TextHelpCommand — текстовая справка по командам test_logger.py # setup_logging — уровни, обработчики, формат test_commands_status.py # команда !status — embed и uptime test_commands_stats.py # команда !stats — подсчёт серверов и каналов @@ -106,14 +110,18 @@ python -m pytest tests/ -v | `test_fetch_weather.py` | `fetch_weather()`, `fetch_open_meteo()` | 19 | | `test_format_articles.py` | `truncate_title()`, `_parse_date()`, `format_articles()` | 20 | | `test_commands_pg.py` | `Pg` cog, команда `!pg` | 13 | -| `test_bot.py` | инициализация бота | 5 | +| `test_commands_cat.py` | `Cat` cog, команда `!cat` (embed, fallback) | 8 | +| `test_commands_news.py` | `News` cog, команда `!nw` (статьи, посты, fallback) | 8 | +| `test_commands_morning.py` | `Morning` cog, команда `!morning` (run_morning) | 5 | +| `test_bot.py` | инициализация бота, обработка ошибок запуска | 5 | | `test_morning_runner.py` | morning runner | 12 | | `test_logger.py` | `setup_logging` (уровни, обработчики, формат) | 9 | +| `test_help_command.py` | `TextHelpCommand` (справка, алиасы, скрытые команды) | 11 | | `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 | | `test_commands_status.py` | команда `!status` (embed, uptime) | 6 | | `test_commands_stats.py` | команда `!stats` (серверы, каналы) | 5 | | `test_integration.py` | загрузка когов, поток команд (моки API) | 9 | -**Итого: 161 тест.** +**Итого: 206 функций (282 тестов с учётом parametrized).** ## Запуск в Docker diff --git a/tests/test_commands_cat.py b/tests/test_commands_cat.py new file mode 100644 index 0000000..b03a87e --- /dev/null +++ b/tests/test_commands_cat.py @@ -0,0 +1,136 @@ +"""Тесты для commands/cat.py — команда !cat.""" + +import sys +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import discord +import pytest + +ROOT_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT_DIR)) + + +def make_context() -> MagicMock: + """Создать мокированный ctx.""" + ctx = MagicMock() + ctx.author.name = "TestUser" + ctx.send = AsyncMock(return_value=None) + return ctx + + +class TestCatInit: + """Тесты инициализации Cat cog.""" + + def test_cat_cog_instantiates(self) -> None: + """Cat должен создаваться без параметров.""" + from commands.cat import Cat + + cog = Cat() + assert cog is not None + + def test_cat_has_command(self) -> None: + """Cat должен содержать команду cat.""" + from commands.cat import Cat + + cog = Cat() + assert cog.cat.name == "cat" + + +class TestCatCommand: + """Тесты команды !cat.""" + + @pytest.mark.asyncio + async def test_cat_success(self) -> None: + """Успешный ответ API -> embed с котиком.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = "https://example.com/cat.jpg" + await cog.cat(cog, ctx) + + ctx.send.assert_awaited_once() + embed = 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_api_returns_none(self) -> None: + """API вернул None -> fallback сообщение.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = None + await cog.cat(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + assert "Не удалось получить котика" in content + + @pytest.mark.asyncio + async def test_cat_embed_color_orange(self) -> None: + """Embed должен быть оранжевого цвета.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = "https://example.com/cat.jpg" + await cog.cat(cog, ctx) + + embed = ctx.send.call_args[1]["embed"] + assert embed.color == discord.Color.orange() + + @pytest.mark.asyncio + async def test_cat_image_set_via_set_image(self) -> None: + """URL должен быть установлен через embed.set_image.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = "https://example.com/cat.jpg" + await cog.cat(cog, ctx) + + embed = ctx.send.call_args[1]["embed"] + assert embed.image is not None + assert embed.image.url == "https://example.com/cat.jpg" + + @pytest.mark.asyncio + async def test_cat_with_special_url(self) -> None: + """URL со спецсимволами должен корректно встраиваться.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = "https://example.com/cat.jpg?size=large&format=webp" + await cog.cat(cog, ctx) + + embed = ctx.send.call_args[1]["embed"] + assert embed.image.url == "https://example.com/cat.jpg?size=large&format=webp" + + @pytest.mark.asyncio + async def test_cat_send_raises(self) -> None: + """Ошибка при отправке — fetch_cat вызывался.""" + from commands.cat import Cat + + cog = Cat() + ctx = make_context() + ctx.send.side_effect = Exception("channel error") + + with patch("commands.cat.fetch_cat", new_callable=AsyncMock) as mock: + mock.return_value = "https://example.com/cat.jpg" + with pytest.raises(Exception, match="channel error"): + await cog.cat(cog, ctx) + + mock.assert_awaited_once() diff --git a/tests/test_commands_morning.py b/tests/test_commands_morning.py new file mode 100644 index 0000000..5bc5480 --- /dev/null +++ b/tests/test_commands_morning.py @@ -0,0 +1,84 @@ +"""Тесты для commands/morning.py — команда !morning.""" + +import sys +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +ROOT_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT_DIR)) + + +def make_context() -> MagicMock: + """Создать мокированный ctx.""" + ctx = MagicMock() + ctx.author.name = "TestUser" + ctx.send = AsyncMock(return_value=None) + ctx.bot = MagicMock() + ctx.channel = MagicMock() + ctx.channel.name = "test-channel" + return ctx + + +class TestMorningInit: + """Тесты инициализации Morning cog.""" + + def test_morning_cog_instantiates(self) -> None: + """Morning должен создаваться без параметров.""" + from commands.morning import Morning + + cog = Morning() + assert cog is not None + + def test_morning_has_command(self) -> None: + """Morning должен содержать команду morning.""" + from commands.morning import Morning + + cog = Morning() + assert cog.morning.name == "morning" + + +class TestMorningCommand: + """Тесты команды !morning.""" + + @pytest.mark.asyncio + async def test_morning_calls_run_morning(self) -> None: + """!morning должен вызвать run_morning.""" + from commands.morning import Morning + + cog = Morning() + ctx = make_context() + + with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run: + await cog.morning(cog, ctx) + + mock_run.assert_awaited_once_with(ctx.bot, ctx.channel) + + @pytest.mark.asyncio + async def test_morning_passes_bot_and_channel(self) -> None: + """!morning передаёт ctx.bot и ctx.channel в run_morning.""" + from commands.morning import Morning + + cog = Morning() + ctx = make_context() + + with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run: + await cog.morning(cog, ctx) + + call_args = mock_run.call_args + assert call_args[0][0] is ctx.bot + assert call_args[0][1] is ctx.channel + + @pytest.mark.asyncio + async def test_morning_run_morning_raises(self) -> None: + """Ошибка в run_morning должна пробрасываться.""" + from commands.morning import Morning + + cog = Morning() + ctx = make_context() + + with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run: + mock_run.side_effect = Exception("api error") + with pytest.raises(Exception, match="api error"): + await cog.morning(cog, ctx) diff --git a/tests/test_commands_news.py b/tests/test_commands_news.py new file mode 100644 index 0000000..6540af3 --- /dev/null +++ b/tests/test_commands_news.py @@ -0,0 +1,151 @@ +"""Тесты для commands/news.py — команда !nw.""" + +import sys +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +ROOT_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT_DIR)) + + +def make_context() -> MagicMock: + """Создать мокированный ctx.""" + ctx = MagicMock() + ctx.author.name = "TestUser" + ctx.send = AsyncMock(return_value=None) + return ctx + + +class TestNewsInit: + """Тесты инициализации News cog.""" + + def test_news_cog_instantiates(self) -> None: + """News должен создаваться без параметров.""" + from commands.news import News + + cog = News() + assert cog is not None + + def test_news_has_command(self) -> None: + """News должен содержать команду nw.""" + from commands.news import News + + cog = News() + assert cog.nw.name == "nw" + + +class TestNewsCommand: + """Тесты команды !nw.""" + + def _make_articles(self, count: int = 3) -> list[dict]: + return [ + {"title": f"Статья {i}", "link": f"https://habr.com/article/{i}", "pub_date": f"Mon, 28 May 2026 10:00:00 +0000", "creator": "author", "tags": []} + for i in range(1, count + 1) + ] + + @pytest.mark.asyncio + async def test_nw_success_articles_and_posts(self) -> None: + """Успешный ответ API для статей и постов.""" + from commands.news import News + + cog = News() + ctx = make_context() + articles = self._make_articles(3) + posts = self._make_articles(2) + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.side_effect = [articles, posts] + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + call_count = mock_rss.await_count + assert call_count == 2 # статьи + посты + + @pytest.mark.asyncio + async def test_nw_articles_none(self) -> None: + """fetch_rss вернул None для статей -> fallback сообщение.""" + from commands.news import News + + cog = News() + ctx = make_context() + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.return_value = None + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + assert "Не удалось получить новости" in content + + @pytest.mark.asyncio + async def test_nw_empty_articles(self) -> None: + """Пустой список статей -> 'Новостей пока нет'.""" + from commands.news import News + + cog = News() + ctx = make_context() + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.return_value = [] + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + assert "Новостей пока нет" in content + + @pytest.mark.asyncio + async def test_nw_posts_none(self) -> None: + """fetch_rss вернул None для постов -> fallback в сообщение.""" + from commands.news import News + + cog = News() + ctx = make_context() + articles = self._make_articles(2) + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.side_effect = [articles, None] + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + assert "Не удалось получить новости" in content + + @pytest.mark.asyncio + async def test_nw_posts_empty(self) -> None: + """Пустой список постов -> 'Новостей пока нет' для постов.""" + from commands.news import News + + cog = News() + ctx = make_context() + articles = self._make_articles(2) + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.side_effect = [articles, []] + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + # Second "Новостей пока нет" for posts section + count = content.count("Новостей пока нет") + assert count >= 1 + + @pytest.mark.asyncio + async def test_nw_limits_articles_to_5(self) -> None: + """Более 5 статей -> truncate_message обрежет.""" + from commands.news import News + + cog = News() + ctx = make_context() + articles = self._make_articles(20) + posts = self._make_articles(20) + + with patch("commands.news.fetch_rss", new_callable=AsyncMock) as mock_rss: + mock_rss.side_effect = [articles, posts] + await cog.nw(cog, ctx) + + ctx.send.assert_awaited_once() + content = ctx.send.call_args[0][0] + # truncate_message limits to 2000 chars for plain text + assert len(content) <= 2003 # 2000 + "..." diff --git a/tests/test_format_articles.py b/tests/test_format_articles.py index 0115613..a62d521 100644 --- a/tests/test_format_articles.py +++ b/tests/test_format_articles.py @@ -1,5 +1,12 @@ import pytest -from utils.news import format_articles, truncate_title, _parse_date +from utils.news import ( + format_articles, + truncate_embed_field, + truncate_embed_text, + truncate_message, + truncate_title, + _parse_date, +) class TestTruncateTitle: @@ -287,3 +294,93 @@ class TestFormatArticles: result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result) == 6 # заголовок + 5 статей assert "Статья 5" not in result[5] + + +class TestTruncateMessage: + """Тесты функции truncate_message() — обрезка plain text сообщений.""" + + def test_short_message_unchanged(self) -> None: + """Короткое сообщение не обрезается.""" + text = "Короткое сообщение" + assert truncate_message(text) == text + + def test_exact_limit_unchanged(self) -> None: + """Текст ровно 2000 символов не обрезается.""" + text = "A" * 2000 + assert truncate_message(text) == text + + def test_over_limit_truncated(self) -> None: + """Текст больше 2000 символов обрезается.""" + text = "A" * 2500 + result = truncate_message(text) + assert len(result) == 2000 + assert result.endswith("...") + + def test_custom_max_len(self) -> None: + """Кастомный max_len.""" + text = "A" * 150 + result = truncate_message(text, max_len=100) + assert len(result) == 100 + assert result.endswith("...") + + def test_under_custom_max_len(self) -> None: + """Текст меньше кастомного max_len не обрезается.""" + text = "A" * 50 + result = truncate_message(text, max_len=100) + assert result == text + + +class TestTruncateEmbedText: + """Тесты функции truncate_embed_text() — обрезка embed.description.""" + + def test_short_text_unchanged(self) -> None: + """Короткий текст не обрезается.""" + text = "Short" + assert truncate_embed_text(text) == text + + def test_exact_limit_unchanged(self) -> None: + """Текст ровно 4096 символов не обрезается.""" + text = "A" * 4096 + assert truncate_embed_text(text) == text + + def test_over_limit_truncated(self) -> None: + """Текст больше 4096 символов обрезается.""" + text = "A" * 5000 + result = truncate_embed_text(text) + assert len(result) == 4096 + assert result.endswith("...") + + def test_custom_max_len(self) -> None: + """Кастомный max_len.""" + text = "A" * 200 + result = truncate_embed_text(text, max_len=100) + assert len(result) == 100 + assert result.endswith("...") + + +class TestTruncateEmbedField: + """Тесты функции truncate_embed_field() — обрезка embed field value.""" + + def test_short_text_unchanged(self) -> None: + """Короткий текст не обрезается.""" + text = "Short" + assert truncate_embed_field(text) == text + + def test_exact_limit_unchanged(self) -> None: + """Текст ровно 1024 символа не обрезается.""" + text = "A" * 1024 + assert truncate_embed_field(text) == text + + def test_over_limit_truncated(self) -> None: + """Текст больше 1024 символов обрезается.""" + text = "A" * 2000 + result = truncate_embed_field(text) + assert len(result) == 1024 + assert result.endswith("...") + + def test_custom_max_len(self) -> None: + """Кастомный max_len.""" + text = "A" * 150 + result = truncate_embed_field(text, max_len=100) + assert len(result) == 100 + assert result.endswith("...") diff --git a/tests/test_help_command.py b/tests/test_help_command.py new file mode 100644 index 0000000..20e7dfa --- /dev/null +++ b/tests/test_help_command.py @@ -0,0 +1,216 @@ +"""Тесты для bot.TextHelpCommand — текстовая справка по командам.""" + +import sys +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +ROOT_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT_DIR)) + + +@pytest.fixture +def help_command() -> "bot.TextHelpCommand": + """Создать экземпляр TextHelpCommand.""" + import bot + return bot.TextHelpCommand() + + +class TestGetCommandSignature: + """Тесты get_command_signature.""" + + def test_simple_command_no_signature(self, help_command) -> None: + """Команда без параметров.""" + cmd = MagicMock() + cmd.qualified_name = "pg" + cmd.signature = "" + result = help_command.get_command_signature(cmd) + assert result == "!pg " + + def test_command_with_signature(self, help_command) -> None: + """Команда с параметрами.""" + cmd = MagicMock() + cmd.qualified_name = "search" + cmd.signature = "" + result = help_command.get_command_signature(cmd) + assert result == "!search " + + def test_group_command(self, help_command) -> None: + """Групповая команда.""" + cmd = MagicMock() + cmd.qualified_name = "mod ban" + cmd.signature = "" + result = help_command.get_command_signature(cmd) + assert result == "!mod ban " + + +class TestSendBotHelp: + """Тесты send_bot_help.""" + + @pytest.mark.asyncio + async def test_send_bot_help_shows_commands(self, help_command) -> None: + """Должен показать список команд по когам.""" + cmd = MagicMock() + cmd.name = "pg" + cmd.hidden = False + cmd.short_doc = "Прогноз погоды" + + cog = MagicMock() + cog.qualified_name = "Pg" + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + mapping = {cog: [cmd], None: []} + await help_command.send_bot_help(mapping) + + destination.send.assert_awaited_once() + message = destination.send.call_args[0][0] + assert "Доступные команды:" in message + assert "!pg - Прогноз погоды" in message + assert "!<название команды>" in message + + @pytest.mark.asyncio + async def test_send_bot_help_skips_hidden(self, help_command) -> None: + """Скрытые команды не должны показываться.""" + cmd = MagicMock() + cmd.name = "hidden_cmd" + cmd.hidden = True + cmd.short_doc = "Скрытая" + + cog = MagicMock() + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + mapping = {cog: [cmd]} + await help_command.send_bot_help(mapping) + + destination.send.assert_awaited_once() + message = destination.send.call_args[0][0] + assert "hidden_cmd" not in message + + @pytest.mark.asyncio + async def test_send_bot_help_skips_none_cog(self, help_command) -> None: + """Команды без cog (None) должны пропускаться.""" + cmd = MagicMock() + cmd.name = "built_in" + cmd.hidden = False + cmd.short_doc = "Встроенная" + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + mapping = {None: [cmd]} + await help_command.send_bot_help(mapping) + + destination.send.assert_awaited_once() + message = destination.send.call_args[0][0] + assert "built_in" not in message + + @pytest.mark.asyncio + async def test_send_bot_help_empty_doc(self, help_command) -> None: + """Команда без doc -> пустая строка описания.""" + cmd = MagicMock() + cmd.name = "cat" + cmd.hidden = False + cmd.short_doc = "" + + cog = MagicMock() + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + mapping = {cog: [cmd]} + await help_command.send_bot_help(mapping) + + message = destination.send.call_args[0][0] + assert "!cat - " in message + + +class TestSendCommandHelp: + """Тесты send_command_help.""" + + @pytest.mark.asyncio + async def test_send_command_help_basic(self, help_command) -> None: + """Базовая справка по команде.""" + cmd = MagicMock() + cmd.qualified_name = "pg" + cmd.signature = "" + cmd.doc = "Прогноз погоды" + cmd.short_doc = "Прогноз погоды" + cmd.aliases = [] + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + await help_command.send_command_help(cmd) + + message = destination.send.call_args[0][0] + assert "!pg " in message + assert "Прогноз погоды" in message + + @pytest.mark.asyncio + async def test_send_command_help_with_aliases(self, help_command) -> None: + """Команда с алиасами.""" + cmd = MagicMock() + cmd.qualified_name = "pg" + cmd.signature = "" + cmd.doc = "Погода" + cmd.short_doc = "Погода" + cmd.aliases = ["weather", "w"] + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + await help_command.send_command_help(cmd) + + message = destination.send.call_args[0][0] + assert "!weather, !w" in message + + +class TestSendCogHelp: + """Тесты send_cog_help.""" + + @pytest.mark.asyncio + async def test_send_cog_help(self, help_command) -> None: + """Справка по когу.""" + cmd = MagicMock() + cmd.name = "pg" + cmd.hidden = False + cmd.short_doc = "Погода" + + cog = MagicMock() + cog.qualified_name = "Pg" + cog.get_commands.return_value = [cmd] + + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + await help_command.send_cog_help(cog) + + message = destination.send.call_args[0][0] + assert "[Pg]:" in message + assert "!pg - Погода" in message + + +class TestSendErrorMessage: + """Тесты send_error_message.""" + + @pytest.mark.asyncio + async def test_send_error_message(self, help_command) -> None: + """Сообщение об ошибке.""" + destination = MagicMock() + destination.send = AsyncMock(return_value=None) + + with patch.object(help_command, "get_destination", return_value=destination): + await help_command.send_error_message("Command not found") + + destination.send.assert_awaited_once_with("Command not found")