feat: кнопка «Ответить» на сообщении оператора
Оператор мог ответить, когда пользователь уже вышел из режима поддержки.
Сообщение приходило, но ответить было нечем: снова идти в «Помощь →
Поддержка», а про прежний контекст уже ничего не напоминало.
- К сообщению оператора добавляется инлайн-кнопка «↩️ Ответить», только
если обращение закрыто (при активном пользователь и так пишет оператору).
- Кнопка ставит is_active=1, убирает себя с сообщения и показывает
клавиатуру поддержки (только «Главное меню»).
- Callback проверяет владельца: support:reply:<user_id> сработает только
для того пользователя, кому пришло сообщение.
- Роутер support.router не затронут фильтром основного роутера
(F.message.chat.type == "private") — он на него не распространяется.
Тесты: 251 OK (7 новых). Проверено, что aiogram вызывает bot(SendMessage(...)),
а не bot.send_message — asserts адаптированы, иначе тест проходил вхолостую.
VERSION: 0.1.0+58 -> 0.1.0+59
This commit is contained in:
1 parent
175d618d62
commit
21f4adf0b1
5 files changed
+178
-7
No files matched your search
@@ -429,6 +429,88 @@ class UserMessageToTopicTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(bot.send_message.await_args.kwargs["text"], "99:\nпривет")
|
||||
|
||||
|
||||
class SupportReplyButtonTests(unittest.IsolatedAsyncioTestCase):
|
||||
"""Кнопка «Ответить» под сообщением оператора, если обращение закрыто."""
|
||||
|
||||
def test_callback_data_carries_user_id(self):
|
||||
self.assertEqual(support.support_reply_callback_data(42), "support:reply:42")
|
||||
|
||||
def test_keyboard_has_single_reply_button(self):
|
||||
markup = support.support_reply_keyboard(42)
|
||||
button = markup.inline_keyboard[0][0]
|
||||
self.assertEqual(button.text, support.REPLY_TO_OPERATOR_BUTTON)
|
||||
self.assertEqual(button.callback_data, "support:reply:42")
|
||||
|
||||
async def test_reply_button_added_when_case_is_closed(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(text="Проверяю платёж", caption=None,
|
||||
chat=SimpleNamespace(id=-100123))
|
||||
with patch.object(support, "is_support_active", new=lambda uid, db_path=None: False):
|
||||
await support._deliver_operator_reply(bot, message, 42)
|
||||
|
||||
self.assertIsNotNone(bot.send_message.await_args.kwargs["reply_markup"])
|
||||
|
||||
async def test_reply_button_omitted_when_case_is_open(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(text="Проверяю платёж", caption=None,
|
||||
chat=SimpleNamespace(id=-100123))
|
||||
with patch.object(support, "is_support_active", new=lambda uid, db_path=None: True):
|
||||
await support._deliver_operator_reply(bot, message, 42)
|
||||
|
||||
self.assertIsNone(bot.send_message.await_args.kwargs["reply_markup"])
|
||||
|
||||
async def test_button_reactivates_support_case(self):
|
||||
bot = AsyncMock()
|
||||
real_message = Message(
|
||||
message_id=1,
|
||||
date=datetime.datetime.now(),
|
||||
chat=Chat(id=555, type="private"),
|
||||
text="Оператор поддержки:\nПроверяю платёж",
|
||||
).as_(bot)
|
||||
callback = SimpleNamespace(
|
||||
data="support:reply:42",
|
||||
from_user=SimpleNamespace(id=42),
|
||||
message=real_message,
|
||||
answer=AsyncMock(),
|
||||
)
|
||||
# Без SUPPORT_GROUP_ID обработчик отказывает — поддержка выключена.
|
||||
with patch.dict("os.environ", {"SUPPORT_GROUP_ID": "-100123"}, clear=False):
|
||||
with patch.object(support, "set_active") as set_active:
|
||||
await support.handle_support_reply(callback)
|
||||
|
||||
set_active.assert_called_once_with(42, True)
|
||||
callback.answer.assert_awaited_once_with()
|
||||
# aiogram вызывает bot(SendMessage(...)), а не bot.send_message напрямую.
|
||||
sent = [c.args[0] for c in bot.mock_calls if c.args and type(c.args[0]).__name__ == "SendMessage"]
|
||||
self.assertEqual(len(sent), 1)
|
||||
self.assertEqual(sent[0].text, support.SUPPORT_REPLY_PROMPT)
|
||||
self.assertIsNotNone(sent[0].reply_markup)
|
||||
|
||||
async def test_button_is_rejected_for_another_user(self):
|
||||
callback = SimpleNamespace(
|
||||
data="support:reply:42",
|
||||
from_user=SimpleNamespace(id=999),
|
||||
message=SimpleNamespace(edit_reply_markup=AsyncMock(), answer=AsyncMock()),
|
||||
answer=AsyncMock(),
|
||||
)
|
||||
with patch.object(support, "set_active") as set_active:
|
||||
await support.handle_support_reply(callback)
|
||||
|
||||
set_active.assert_not_called()
|
||||
callback.message.answer.assert_not_awaited()
|
||||
|
||||
async def test_button_is_rejected_on_broken_callback_data(self):
|
||||
callback = SimpleNamespace(
|
||||
data="support:reply:abc",
|
||||
from_user=SimpleNamespace(id=42),
|
||||
message=SimpleNamespace(edit_reply_markup=AsyncMock(), answer=AsyncMock()),
|
||||
answer=AsyncMock(),
|
||||
)
|
||||
with patch.object(support, "set_active") as set_active:
|
||||
await support.handle_support_reply(callback)
|
||||
set_active.assert_not_called()
|
||||
|
||||
|
||||
class OperatorSignatureTests(unittest.IsolatedAsyncioTestCase):
|
||||
"""Ответ оператора приходит пользователю с подписью «Оператор поддержки:»."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user