surfaces/tests/adapter/telegram/test_forum.py

374 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
from aiogram.fsm.context import FSMContext
from aiogram.types import Chat, MessageOriginChat
from adapter.telegram.converter import is_forum_message, resolve_forum_chat_id
from adapter.telegram.handlers import chat as chat_handler
from adapter.telegram.handlers import confirm as confirm_handler
from adapter.telegram.handlers import forum as forum_handler
from adapter.telegram.states import ChatState, ForumSetupState
from core.protocol import OutgoingMessage
def make_message(*, text: str = "hello", thread_id: int | None = None):
message = SimpleNamespace()
message.text = text
message.caption = None
message.photo = None
message.document = None
message.voice = None
message.message_thread_id = thread_id
message.chat = SimpleNamespace(id=-100123)
message.from_user = SimpleNamespace(id=42, full_name="Alice", first_name="Alice")
message.answer = AsyncMock()
message.edit_text = AsyncMock()
message.edit_reply_markup = AsyncMock()
message.bot = SimpleNamespace(
send_message=AsyncMock(),
send_chat_action=AsyncMock(),
create_forum_topic=AsyncMock(),
get_me=AsyncMock(),
get_chat_member=AsyncMock(),
)
message.chat_shared = None
return message
class FakeTask:
def cancel(self) -> None:
self.cancelled = True
def __await__(self):
async def _done():
return None
return _done().__await__()
async def test_forum_helpers_detect_and_resolve(monkeypatch):
message = make_message(thread_id=77)
monkeypatch.setattr(
chat_handler.db,
"get_chat_by_thread",
lambda tg_user_id, thread_id: {"chat_id": "chat-77"} if thread_id == 77 else None,
)
assert is_forum_message(message) is True
assert resolve_forum_chat_id(message, 42) == "chat-77"
async def test_cmd_forum_enters_setup_state():
message = make_message(text="/forum")
state = AsyncMock(spec=FSMContext)
await forum_handler.cmd_forum(message, state)
state.set_state.assert_awaited_once_with(ForumSetupState.waiting_for_group)
message.answer.assert_awaited_once()
assert message.answer.await_args.kwargs["reply_markup"] is not None
async def test_handle_group_forward_registers_group_and_topics(monkeypatch):
message = make_message(text="forwarded")
message.forward_from_chat = SimpleNamespace(id=-100200, type="supergroup", title="Lambda")
message.bot.get_me.return_value = SimpleNamespace(id=999)
message.bot.get_chat_member.return_value = SimpleNamespace(
status="administrator",
can_manage_topics=True,
)
message.bot.create_forum_topic.side_effect = [
SimpleNamespace(message_thread_id=11),
SimpleNamespace(message_thread_id=22),
]
state = AsyncMock(spec=FSMContext)
monkeypatch.setattr(
forum_handler.db,
"get_user_chats",
lambda tg_user_id: [
{"chat_id": "chat-1", "name": "One", "forum_thread_id": None},
{"chat_id": "chat-2", "name": "Two", "forum_thread_id": None},
],
)
set_forum_group = Mock()
set_forum_thread = Mock()
monkeypatch.setattr(forum_handler.db, "set_forum_group", set_forum_group)
monkeypatch.setattr(forum_handler.db, "set_forum_thread", set_forum_thread)
await forum_handler.handle_group_forward(message, state)
set_forum_group.assert_called_once_with(42, -100200)
assert message.bot.create_forum_topic.await_count == 2
set_forum_thread.assert_any_call("chat-1", 11)
set_forum_thread.assert_any_call("chat-2", 22)
state.set_state.assert_awaited_once_with(ChatState.idle)
assert "Группа подключена" in message.answer.await_args.args[0]
async def test_handle_group_forward_accepts_forward_origin_chat(monkeypatch):
message = make_message(text="forwarded")
message.forward_from_chat = None
message.forward_origin = MessageOriginChat(
date=datetime.now(),
sender_chat=Chat(id=-100200, type="supergroup", title="Lambda", is_forum=True),
)
message.bot.get_me.return_value = SimpleNamespace(id=999)
message.bot.get_chat_member.return_value = SimpleNamespace(
status="administrator",
can_manage_topics=True,
)
state = AsyncMock(spec=FSMContext)
monkeypatch.setattr(forum_handler.db, "get_user_chats", lambda tg_user_id: [])
set_forum_group = Mock()
monkeypatch.setattr(forum_handler.db, "set_forum_group", set_forum_group)
await forum_handler.handle_group_forward(message, state)
set_forum_group.assert_called_once_with(42, -100200)
state.set_state.assert_awaited_once_with(ChatState.idle)
assert "Группа подключена" in message.answer.await_args.args[0]
async def test_handle_group_forward_accepts_chat_shared(monkeypatch):
message = make_message(text="selected")
message.chat_shared = SimpleNamespace(request_id=1, chat_id=-100200, title="Lambda")
message.bot.get_me.return_value = SimpleNamespace(id=999)
message.bot.get_chat_member.return_value = SimpleNamespace(
status="administrator",
can_manage_topics=True,
)
state = AsyncMock(spec=FSMContext)
monkeypatch.setattr(forum_handler.db, "get_user_chats", lambda tg_user_id: [])
set_forum_group = Mock()
monkeypatch.setattr(forum_handler.db, "set_forum_group", set_forum_group)
await forum_handler.handle_group_forward(message, state)
set_forum_group.assert_called_once_with(42, -100200)
state.set_state.assert_awaited_once_with(ChatState.idle)
assert "Группа подключена" in message.answer.await_args.args[0]
async def test_handle_group_forward_reports_missing_forward_metadata():
message = make_message(text="not forwarded")
message.forward_from_chat = None
message.forward_origin = None
state = AsyncMock(spec=FSMContext)
await forum_handler.handle_group_forward(message, state)
message.answer.assert_awaited_once()
assert "данных о группе" in message.answer.await_args.args[0]
state.set_state.assert_not_awaited()
async def test_handle_group_forward_reports_non_forum_supergroup():
message = make_message(text="forwarded")
message.forward_from_chat = SimpleNamespace(
id=-100200,
type="supergroup",
title="Lambda",
is_forum=False,
)
state = AsyncMock(spec=FSMContext)
await forum_handler.handle_group_forward(message, state)
message.answer.assert_awaited_once()
assert "выключены Topics" in message.answer.await_args.args[0]
state.set_state.assert_not_awaited()
async def test_handle_message_routes_forum_thread(monkeypatch):
message = make_message(thread_id=77)
dispatcher = SimpleNamespace(
dispatch=AsyncMock(
return_value=[OutgoingMessage(chat_id="chat-77", text="ok")]
)
)
state = AsyncMock(spec=FSMContext)
state.get_data.return_value = {}
monkeypatch.setattr(
chat_handler.db,
"get_or_create_tg_user",
lambda tg_user_id, platform_user_id, display_name: {
"platform_user_id": "usr-42",
"display_name": display_name,
},
)
monkeypatch.setattr(
chat_handler.db,
"get_chat_by_thread",
lambda tg_user_id, thread_id: {"chat_id": "chat-77", "name": "Forum chat"},
)
monkeypatch.setattr(
chat_handler.db,
"get_chat_by_id",
lambda chat_id: {"chat_id": chat_id, "name": "Forum chat"},
)
monkeypatch.setattr(
chat_handler.asyncio,
"create_task",
lambda coro: (coro.close(), FakeTask())[1],
)
await chat_handler.handle_message(message, state, dispatcher)
incoming = dispatcher.dispatch.await_args.args[0]
assert incoming.chat_id == "chat-77"
assert incoming.user_id == "usr-42"
assert state.update_data.await_args.kwargs == {
"active_chat_id": "chat-77",
"active_chat_name": "Forum chat",
}
message.bot.send_message.assert_awaited_once()
assert message.bot.send_message.await_args.args[0] == -100123
assert message.bot.send_message.await_args.kwargs["message_thread_id"] == 77
assert message.bot.send_message.await_args.args[1] == "ok"
async def test_cmd_new_chat_creates_forum_topic_for_dm(monkeypatch):
message = make_message(text="/new Analysis")
state = AsyncMock(spec=FSMContext)
message.bot.create_forum_topic.return_value = SimpleNamespace(message_thread_id=333)
monkeypatch.setattr(chat_handler.db, "get_forum_group", lambda tg_user_id: -100200)
monkeypatch.setattr(chat_handler.db, "count_chats", lambda tg_user_id: 2)
create_chat = Mock(return_value="chat-3")
set_forum_thread = Mock()
monkeypatch.setattr(chat_handler.db, "create_chat", create_chat)
monkeypatch.setattr(chat_handler.db, "set_forum_thread", set_forum_thread)
await chat_handler.cmd_new_chat(message, state)
create_chat.assert_called_once_with(42, "Analysis")
message.bot.create_forum_topic.assert_awaited_once_with(chat_id=-100200, name="Analysis")
set_forum_thread.assert_called_once_with("chat-3", 333)
state.update_data.assert_awaited_once_with(active_chat_id="chat-3", active_chat_name="Analysis")
message.answer.assert_awaited_once()
assert "Форум-тема тоже создана" in message.answer.await_args.args[0]
async def test_cmd_new_chat_registers_topic(monkeypatch):
message = make_message(text="/new Research", thread_id=88)
state = AsyncMock(spec=FSMContext)
monkeypatch.setattr(
chat_handler.db,
"get_chat_by_thread",
lambda tg_user_id, thread_id: None,
)
monkeypatch.setattr(chat_handler.db, "count_chats", lambda tg_user_id: 4)
create_chat = Mock(return_value="chat-5")
set_forum_thread = Mock()
monkeypatch.setattr(chat_handler.db, "create_chat", create_chat)
monkeypatch.setattr(chat_handler.db, "set_forum_thread", set_forum_thread)
await chat_handler.cmd_new_chat(message, state)
create_chat.assert_called_once_with(42, "Research")
set_forum_thread.assert_called_once_with("chat-5", 88)
message.bot.send_message.assert_awaited_once()
assert message.bot.send_message.await_args.kwargs["message_thread_id"] == 88
state.update_data.assert_awaited_once_with(active_chat_id="chat-5", active_chat_name="Research")
async def test_cmd_list_chats_rejected_in_forum_topic():
message = make_message(text="/chats", thread_id=88)
state = AsyncMock(spec=FSMContext)
await chat_handler.cmd_list_chats(message, state)
message.bot.send_message.assert_awaited_once()
assert message.bot.send_message.await_args.kwargs["message_thread_id"] == 88
assert "отключено" in message.bot.send_message.await_args.args[1]
async def test_switch_chat_rejected_in_forum_topic():
callback = SimpleNamespace(
data="switch:chat-9:Other",
from_user=SimpleNamespace(id=42, full_name="Alice"),
message=make_message(thread_id=88),
answer=AsyncMock(),
)
state = AsyncMock(spec=FSMContext)
await chat_handler.switch_chat(callback, state)
state.update_data.assert_not_awaited()
callback.answer.assert_awaited_once_with(
"Переключение чатов доступно только в личке с ботом.",
show_alert=True,
)
async def test_new_chat_callback_rejected_in_forum_topic(monkeypatch):
callback = SimpleNamespace(
data="new_chat",
from_user=SimpleNamespace(id=42, full_name="Alice"),
message=make_message(thread_id=88),
answer=AsyncMock(),
)
state = AsyncMock(spec=FSMContext)
create_chat = Mock()
monkeypatch.setattr(chat_handler.db, "create_chat", create_chat)
await chat_handler.cb_new_chat(callback, state)
create_chat.assert_not_called()
state.update_data.assert_not_awaited()
callback.answer.assert_awaited_once_with(
"Создание нового чата из списка доступно только в личке с ботом.",
show_alert=True,
)
async def test_confirm_callback_routes_back_to_forum_thread(monkeypatch):
message = make_message(thread_id=77)
callback = SimpleNamespace(
data="confirm:yes:action-1",
from_user=message.from_user,
message=message,
answer=AsyncMock(),
)
dispatcher = SimpleNamespace(
dispatch=AsyncMock(
return_value=[OutgoingMessage(chat_id="chat-77", text="done")]
)
)
state = AsyncMock(spec=FSMContext)
state.get_data.return_value = {}
monkeypatch.setattr(
confirm_handler.db,
"get_or_create_tg_user",
lambda tg_user_id, platform_user_id, display_name: {
"platform_user_id": "usr-42",
"display_name": display_name,
},
)
monkeypatch.setattr(
confirm_handler.db,
"get_chat_by_thread",
lambda tg_user_id, thread_id: {"chat_id": "chat-77"},
)
monkeypatch.setattr(
confirm_handler.db,
"get_chat_by_id",
lambda chat_id: {"chat_id": chat_id, "name": "Forum chat"},
)
await confirm_handler.handle_confirm(callback, state, dispatcher)
assert dispatcher.dispatch.await_args.args[0].chat_id == "chat-77"
assert callback.message.bot.send_message.await_count == 1
assert callback.message.bot.send_message.await_args.args[1] == "done"
assert callback.message.bot.send_message.await_args.kwargs["message_thread_id"] == 77