374 lines
13 KiB
Python
374 lines
13 KiB
Python
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
|