import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, patch import bot import keyboards class ButtonTextTests(unittest.TestCase): def test_buttons_map_to_commands_and_requests(self): self.assertEqual(keyboards.resolve_button_text("📖 Помощь"), "/help") self.assertEqual(keyboards.resolve_button_text("👤 Баланс"), "/balance") self.assertEqual(keyboards.resolve_button_text("💳 Подписка"), "/subscribe") self.assertEqual(keyboards.resolve_button_text("🎨 Мой стиль"), "/style") def test_idea_button_sends_request_not_command(self): self.assertEqual( keyboards.resolve_button_text("💡 Идея"), "предложи идею для нового поста", ) def test_regular_text_is_not_treated_as_button(self): self.assertIsNone(keyboards.resolve_button_text("Напиши пост про запуск")) self.assertIsNone(keyboards.resolve_button_text("привет")) self.assertIsNone(keyboards.resolve_button_text(None)) def test_button_lookup_tolerates_whitespace(self): self.assertEqual(keyboards.resolve_button_text(" 📖 Помощь "), "/help") class KeyboardLayoutTests(unittest.TestCase): def test_main_menu_has_core_actions(self): labels = [b.text for row in keyboards.MAIN_MENU.keyboard for b in row] for expected in ("✍️ Написать пост", "💡 Идея", "🎨 Мой стиль", "📖 Помощь"): self.assertIn(expected, labels) def test_locked_menu_offers_payment_first(self): self.assertEqual(keyboards.LOCKED_MENU.keyboard[0][0].text, "💳 Оплатить подписку") def test_every_button_is_resolvable(self): for markup in (keyboards.MAIN_MENU, keyboards.LOCKED_MENU): for row in markup.keyboard: for button in row: self.assertIsNotNone( keyboards.resolve_button_text(button.text), button.text ) def test_keyboards_are_resize_keyboard(self): self.assertTrue(keyboards.MAIN_MENU.resize_keyboard) self.assertTrue(keyboards.LOCKED_MENU.resize_keyboard) class _StubMessage: """Минимальный Message: нужны from_user, text и собираемые ответы.""" def __init__(self, text=None, user_id=42, chat_id=555): self.text = text self.from_user = SimpleNamespace(id=user_id) self.chat = SimpleNamespace(id=chat_id) self.message_thread_id = None self.bot = AsyncMock() self.answers: list[tuple[str, dict]] = [] async def answer(self, text, **kwargs): self.answers.append((text, kwargs)) class StyleCommandTests(unittest.IsolatedAsyncioTestCase): async def test_style_without_tone_explains_how_to_set_it(self): message = _StubMessage() with patch.object(bot.asyncio, "to_thread", new=AsyncMock(side_effect=["", []])): await bot.style_command(message) self.assertIn("скриншот", message.answers[0][0]) async def test_style_shows_saved_tone(self): message = _StubMessage() with patch.object(bot.asyncio, "to_thread", new=AsyncMock(side_effect=["Короткие фразы", ["a", "b"]])): await bot.style_command(message) self.assertIn("Короткие фразы", message.answers[0][0]) self.assertIn("Образцов сохранено: 2", message.answers[1][0]) class ButtonRoutingTests(unittest.IsolatedAsyncioTestCase): async def test_button_with_command_routes_to_command_handler(self): message = _StubMessage("📖 Помощь") with patch.object(bot, "help_command", new=AsyncMock()) as help_cmd: await bot.handle_text(message) help_cmd.assert_awaited_once_with(message) async def test_button_with_request_goes_to_classifier(self): message = _StubMessage("💡 Идея") with ( patch.object(bot, "notify_development_deploy", new=AsyncMock()), patch.object(bot, "_classify_and_respond", new=AsyncMock()) as classify, ): await bot.handle_text(message) classify.assert_awaited_once_with(message, "предложи идею для нового поста") async def test_write_post_button_sends_topic_request(self): message = _StubMessage("✍️ Написать пост") with ( patch.object(bot, "notify_development_deploy", new=AsyncMock()), patch.object(bot, "_classify_and_respond", new=AsyncMock()) as classify, ): await bot.handle_text(message) classify.assert_awaited_once_with(message, "Напиши пост") async def test_plain_text_is_not_rewritten(self): message = _StubMessage("Напиши пост про запуск продукта") with ( patch.object(bot, "notify_development_deploy", new=AsyncMock()), patch.object(bot, "_classify_and_respond", new=AsyncMock()) as classify, ): await bot.handle_text(message) classify.assert_awaited_once_with(message, "Напиши пост про запуск продукта") async def test_style_command_is_routed(self): message = _StubMessage("/style") with patch.object(bot, "style_command", new=AsyncMock()) as style: await bot.handle_command_text(message) style.assert_awaited_once_with(message) class MenuPublishTests(unittest.IsolatedAsyncioTestCase): async def test_commands_are_published_to_telegram(self): fake_bot = AsyncMock() await bot.set_main_menu(fake_bot) names = [c.command for c in fake_bot.set_my_commands.await_args.kwargs["commands"]] self.assertIn("help", names) self.assertIn("subscribe", names) self.assertIn("style", names) async def test_publish_failure_does_not_crash_startup(self): fake_bot = AsyncMock() fake_bot.set_my_commands.side_effect = RuntimeError("boom") await bot.set_main_menu(fake_bot) if __name__ == "__main__": unittest.main()