feat: reply-клавиатура + фикс пропавший _send_idea
- keyboards.py: MAIN_MENU (✍️ Написать пост / 💡 Идея / 🎨 Мой стиль / 📖 Помощь / 💳 Подписка / 👤 Баланс) и LOCKED_MENU — при блокировке первой кнопкой идёт «💳 Оплатить подписку» - кнопки не обходят роутинг: resolve_button_text превращает их в slash-команду (/help, /balance, /subscribe, /style) или готовый запрос («Напиши пост», «предложи идею…») - новая команда /style показывает сохранённый Tone of Voice и подсказывает, как его настроить - set_my_commands публикует slash-команды при старте, ошибка не роняет бот - fix: _send_idea вызывалась на bot.py, но была потеряна в коммите про подписку — /idea падал с NameError; функция восстановлена - 17 новых тестов клавиатуры; 92 теста OK; VERSION 0.1.0+26
This commit is contained in:
1 parent
3c8917c20a
commit
971bf115da
7 files changed
+312
-11
No files matched your search
@@ -0,0 +1,140 @@
|
||||
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()
|
||||
Reference in new issue
Block a user