Files
social-post-writer-bot/tests/test_release_help.py
T
admin b586d3aa36 feat: полноширинные кнопки справки + переход в любой раздел
- каждая кнопка в отдельном <tg-button-row align="left"> — во всю ширину сообщения
- из любого раздела доступны кнопки на все остальные (было: «предыдущий»/«следующий»)
- текущий раздел рендерится как <tg-button type="disabled">, чтобы не было активной кнопки на себя
- «⌂ В меню» остаётся отдельной полноширинной кнопкой
- 59 тестов, VERSION 0.1.0+24
2026-10-07 09:52:57 +02:00

192 lines
7.6 KiB
Python

from __future__ import annotations
import asyncio
import unittest
from datetime import datetime
from unittest.mock import AsyncMock, patch
from aiogram.types import Chat, Message
import bot
class DeployNoticeTests(unittest.IsolatedAsyncioTestCase):
async def test_sends_once_per_new_version_only_in_development(self):
fake_bot = AsyncMock()
with (
patch.object(bot, "DEPLOY_ENV", "development"),
patch.object(bot, "VERSION", "0.1.0+5"),
patch.dict("os.environ", {"DEV_CHAT_ID": "2201809732"}, clear=False),
patch.object(bot, "get_deploy_notice_version", return_value="0.1.0+4"),
patch.object(bot, "set_deploy_notice_version"),
patch.object(bot.asyncio, "to_thread", new=AsyncMock(side_effect=["0.1.0+4", None, "0.1.0+5"])),
):
await bot.notify_development_deploy(fake_bot)
await bot.notify_development_deploy(fake_bot)
fake_bot.send_message.assert_awaited_once_with(chat_id=2201809732, text="🚀 Бот обновлён\nВерсия: 0.1.0+5")
async def test_does_not_send_outside_development(self):
fake_bot = AsyncMock()
with patch.object(bot, "DEPLOY_ENV", "production"):
await bot.notify_development_deploy(fake_bot)
fake_bot.send_message.assert_not_awaited()
async def test_deploy_notice_does_not_block_if_test_api_hangs(self):
fake_bot = AsyncMock()
fake_bot.send_message.side_effect = asyncio.TimeoutError
with (
patch.object(bot, "DEPLOY_ENV", "development"),
patch.dict("os.environ", {"DEV_CHAT_ID": "2201809732"}, clear=False),
patch.object(bot, "get_deploy_notice_version", return_value="old"),
):
await bot.notify_development_deploy(fake_bot)
fake_bot.send_message.assert_awaited_once()
class HelpRoutingTests(unittest.TestCase):
def test_commands_are_routed_in_command_handler(self):
self.assertEqual(bot.HELP_COMMANDS, {"help", "помощь"})
from pathlib import Path
source = Path(bot.__file__).read_text(encoding="utf-8")
self.assertIn("async def handle_command_text", source)
self.assertIn("if command == \"/help\"", source)
self.assertIn("elif command == \"/start\"", source)
self.assertIn("elif command == \"/clear\"", source)
self.assertIn("elif command == \"/idea\"", source)
self.assertNotIn("idea_voice", source)
def test_help_sections_cover_usage_scenarios(self):
from help_message import HELP_SECTIONS, help_plain_view, rich_to_plain
text = "\n".join(rich_to_plain(body) for _title, body in HELP_SECTIONS.values())
self.assertIn("/clear", text)
self.assertIn("/idea", text)
self.assertNotIn("/idea_voice", text)
for term in ("пост", "голосовое", "скриншот", "реплаем", "/help"):
self.assertIn(term, text)
menu = help_plain_view(None)
for title, _body in HELP_SECTIONS.values():
self.assertIn(title, menu)
class HelpNavigationTests(unittest.TestCase):
def test_menu_lists_every_section_as_full_width_button(self):
from help_message import HELP_SECTIONS, HELP_SECTION_ORDER, help_menu_html
menu = help_menu_html()
rows = menu.split("<tg-button-row")[1:]
self.assertEqual(len(rows), len(HELP_SECTION_ORDER))
for row in rows:
self.assertTrue(row.startswith(' align="left">'), row)
self.assertEqual(row.count("<tg-button "), 1, row)
for section in HELP_SECTIONS:
self.assertIn(f'data="help:{section}"', menu)
self.assertNotIn('data="help:menu"', menu)
def test_section_navigates_to_every_other_section(self):
from help_message import HELP_SECTIONS, help_section_html
for section in HELP_SECTIONS:
html = help_section_html(section)
self.assertIn('data="help:menu"', html)
for other in HELP_SECTIONS:
if other == section:
continue
self.assertIn(f'data="help:{other}"', html, section)
def test_current_section_is_disabled(self):
from help_message import HELP_SECTIONS, help_section_html
html = help_section_html("tone")
self.assertIn('<tg-button type="disabled">🎨 Tone of Voice</tg-button>', html)
self.assertNotIn('data="help:tone"', html)
for section in HELP_SECTIONS:
if section != "tone":
self.assertIn(f'data="help:{section}" style="primary"', html)
def test_unknown_section_falls_back_to_menu(self):
from help_message import help_view
self.assertEqual(help_view("nope"), help_view(None))
def test_plain_fallback_has_menu_and_section_text(self):
from help_message import help_plain_view
self.assertIn("Помощь по боту", help_plain_view(None))
self.assertIn("Написать пост", help_plain_view("posts"))
class OnboardingTests(unittest.IsolatedAsyncioTestCase):
async def test_start_sends_welcome_rich_then_help_menu(self):
message = AsyncMock()
message.from_user.id = 42
message.message_thread_id = None
calls: list[str] = []
async def fake_rich(_message, html, _stage):
calls.append(html)
with (
patch.object(bot, "notify_development_deploy", new=AsyncMock()),
patch.object(bot, "ensure_subscription"),
patch.object(bot.asyncio, "to_thread", new=AsyncMock()),
patch.object(bot, "send_rich_message", new=fake_rich),
):
await bot.start(message)
self.assertEqual(calls, [bot.WELCOME_RICH_HTML, bot.help_view(None)])
def _callback(self, data: str, message: Message | None):
from aiogram.types import CallbackQuery
callback = AsyncMock(spec=CallbackQuery)
callback.data = data
callback.answer = AsyncMock()
callback.message = message
return callback
async def test_help_callback_edits_rich_message(self):
message = Message(
message_id=77,
date=datetime.now(),
chat=Chat(id=5, type="private"),
)
callback = self._callback("help:tone", message)
with patch.object(bot, "edit_rich_message", new=AsyncMock(return_value=True)) as edit:
await bot.help_navigation(callback)
edit.assert_awaited_once_with(message, bot.help_view("tone"))
callback.answer.assert_awaited_once_with()
async def test_help_callback_reports_error_when_edit_fails(self):
message = Message(
message_id=78,
date=datetime.now(),
chat=Chat(id=5, type="private"),
)
callback = self._callback("help:menu", message)
with patch.object(bot, "edit_rich_message", new=AsyncMock(return_value=False)):
await bot.help_navigation(callback)
callback.answer.assert_awaited_once_with("Не удалось обновить справку, открой /help заново", show_alert=True)
async def test_help_callback_ignores_inaccessible_message(self):
callback = self._callback("help:posts", None)
with patch.object(bot, "edit_rich_message", new=AsyncMock()) as edit:
await bot.help_navigation(callback)
edit.assert_not_awaited()
def test_welcome_message_has_examples_and_help_handoff(self):
from help_message import WELCOME_RICH_HTML, rich_to_plain
text = rich_to_plain(WELCOME_RICH_HTML)
for term in ("Привет", "Напиши пост", "скриншот"):
self.assertIn(term, text)
if __name__ == "__main__":
unittest.main()