Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+155
View File
@@ -0,0 +1,155 @@
"""
Тесты для ContextMenuManager.
"""
import pytest
from unittest.mock import Mock, AsyncMock
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
from core.menu_manager import ContextMenuManager, MenuConfig
from core.permissions import UserRole
class TestContextMenuManager:
"""Тесты менеджера контекстных меню"""
@pytest.fixture
def mock_formatter(self):
"""Мок-объект для KeyboardFormatter"""
formatter = Mock()
formatter.create_main_menu.return_value = InlineKeyboardMarkup([
[InlineKeyboardButton("📋 Помощь", callback_data='menu_help')]
])
formatter.create_games_menu.return_value = InlineKeyboardMarkup([
[InlineKeyboardButton("🎯 Игра 1", callback_data='game_1')]
])
formatter.create_admin_menu.return_value = InlineKeyboardMarkup([
[InlineKeyboardButton("👥 Пользователи", callback_data='admin_users')]
])
return formatter
@pytest.fixture
def mock_permission_manager(self):
"""Мок-объект для PermissionManager"""
pm = Mock()
pm.has_permission.return_value = True
return pm
@pytest.fixture
def menu_manager(self, mock_permission_manager, mock_formatter):
"""Фикстура для ContextMenuManager"""
return ContextMenuManager(mock_permission_manager, mock_formatter)
def test_initialization(self, menu_manager):
"""Тест инициализации менеджера меню"""
assert menu_manager.menus is not None
assert 'menu_main' in menu_manager.menus
assert 'menu_admin' in menu_manager.menus
# Проверка разделения по уровням
assert 'user' in menu_manager.menu_levels
assert 'admin' in menu_manager.menu_levels
assert 'super_admin' in menu_manager.menu_levels
def test_menu_registration(self, menu_manager):
"""Тест регистрации нового меню"""
async def test_builder(role, **context):
return InlineKeyboardMarkup([[InlineKeyboardButton("Test", callback_data='test')]])
menu_manager.register_menu(
'test_menu', test_builder, UserRole.USER, 'user', 'Тестовое меню'
)
assert 'test_menu' in menu_manager.menus
config = menu_manager.menus['test_menu']
assert config.required_role == UserRole.USER
assert config.level == 'user'
assert config.description == 'Тестовое меню'
@pytest.mark.asyncio
async def test_get_menu_for_user_success(self, menu_manager):
"""Тест успешного получения меню для пользователя"""
menu = await menu_manager.get_menu_for_user('menu_main', UserRole.USER)
assert menu is not None
assert isinstance(menu, InlineKeyboardMarkup)
assert len(menu.inline_keyboard) > 0
@pytest.mark.asyncio
async def test_get_menu_for_user_insufficient_permissions(self, menu_manager, mock_permission_manager):
"""Тест получения меню при недостаточных правах"""
mock_permission_manager.has_permission.return_value = False
menu = await menu_manager.get_menu_for_user('menu_admin', UserRole.USER)
assert menu is None
@pytest.mark.asyncio
async def test_get_menu_for_user_unknown_menu(self, menu_manager):
"""Тест получения неизвестного меню"""
menu = await menu_manager.get_menu_for_user('unknown_menu', UserRole.USER)
assert menu is None
def test_is_menu_available(self, menu_manager, mock_permission_manager):
"""Тест проверки доступности меню"""
# Доступно
mock_permission_manager.has_permission.return_value = True
assert menu_manager.is_menu_available('menu_main', UserRole.USER)
# Недоступно
mock_permission_manager.has_permission.return_value = False
assert not menu_manager.is_menu_available('menu_admin', UserRole.USER)
def test_is_menu_available_unknown_menu(self, menu_manager):
"""Тест проверки доступности неизвестного меню"""
assert not menu_manager.is_menu_available('unknown_menu', UserRole.USER)
def test_get_available_menus_for_role(self, menu_manager, mock_permission_manager):
"""Тест получения списка доступных меню для роли"""
mock_permission_manager.has_permission.return_value = True
menus = menu_manager.get_available_menus_for_role(UserRole.USER)
assert isinstance(menus, list)
assert 'menu_main' in menus
assert 'menu_help' in menus
def test_get_menus_by_level(self, menu_manager):
"""Тест получения меню по уровням"""
user_menus = menu_manager.get_menus_by_level('user')
admin_menus = menu_manager.get_menus_by_level('admin')
assert isinstance(user_menus, list)
assert isinstance(admin_menus, list)
assert 'menu_main' in user_menus
assert 'menu_admin' in admin_menus
def test_get_menus_by_unknown_level(self, menu_manager):
"""Тест получения меню для неизвестного уровня"""
menus = menu_manager.get_menus_by_level('unknown')
assert menus == []
def test_clear_cache(self, menu_manager):
"""Тест очистки кеша"""
# Добавляем что-то в кеш
menu_manager.menu_cache['test'] = Mock()
assert len(menu_manager.menu_cache) > 0
menu_manager.clear_cache()
assert len(menu_manager.menu_cache) == 0
def test_menu_config(self):
"""Тест конфигурации меню"""
async def handler():
pass
config = MenuConfig('test', handler, UserRole.ADMIN, 'admin', 'Test menu')
assert config.menu_id == 'test'
assert config.required_role == UserRole.ADMIN
assert config.level == 'admin'
assert config.description == 'Test menu'
+297
View File
@@ -0,0 +1,297 @@
"""
Тесты для MessageTypeRouter.
"""
import pytest
from unittest.mock import Mock, AsyncMock
from telegram import Update, Message, User, CallbackQuery
from core.message_router import MessageTypeRouter, MessageHandlerConfig
from core.permissions import UserRole
class TestMessageTypeRouter:
"""Тесты маршрутизатора типов сообщений"""
@pytest.fixture
def message_router(self):
"""Фикстура для MessageTypeRouter"""
return MessageTypeRouter()
@pytest.fixture
def mock_update_text(self):
"""Мок-объект для текстового сообщения"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
user.id = 123456789
message.text = "Hello world"
message.voice = None
message.photo = None
update.message = message
update.effective_user = user
# Mock chat for permission checking
chat = Mock()
member = Mock()
member.status = 'member'
chat.get_member = AsyncMock(return_value=member)
update.effective_chat = chat
return update
@pytest.fixture
def mock_update_voice(self):
"""Мок-объект для голосового сообщения"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
user.id = 123456789
voice = Mock()
message.text = None
message.voice = voice
message.photo = None
update.message = message
update.effective_user = user
# Mock chat for permission checking
chat = Mock()
member = Mock()
member.status = 'member'
chat.get_member = AsyncMock(return_value=member)
update.effective_chat = chat
return update
@pytest.fixture
def mock_update_callback(self):
"""Мок-объект для callback запроса"""
update = Mock(spec=Update)
callback_query = Mock(spec=CallbackQuery)
user = Mock(spec=User)
user.id = 123456789
callback_query.data = "test_callback"
update.callback_query = callback_query
update.effective_user = user
# Mock chat for permission checking
chat = Mock()
member = Mock()
member.status = 'member'
chat.get_member = AsyncMock(return_value=member)
update.effective_chat = chat
return update
@pytest.fixture
def mock_context(self):
"""Мок-объект для контекста"""
return Mock()
def test_initialization(self, message_router):
"""Тест инициализации маршрутизатора"""
assert message_router.text_handlers == {}
assert message_router.callback_handlers == {}
assert message_router.voice_handlers == []
assert len(message_router.photo_handlers) == 0
def test_register_text_handler(self, message_router):
"""Тест регистрации обработчика текста"""
async def handler(update, context):
pass
message_router.register_text_handler(
r'hello', handler, UserRole.USER, 1, 'Test handler'
)
assert len(message_router.text_handlers) == 1
pattern, config = list(message_router.text_handlers.items())[0]
assert config.required_role == UserRole.USER
assert config.priority == 1
assert config.description == 'Test handler'
def test_register_text_handler_invalid_regex(self, message_router):
"""Тест регистрации обработчика с невалидным regex"""
async def handler(update, context):
pass
# Неверный regex не должен вызывать исключение, но и не регистрироваться
initial_count = len(message_router.text_handlers)
message_router.register_text_handler(
r'[invalid', handler, UserRole.USER
)
assert len(message_router.text_handlers) == initial_count
def test_register_callback_handler(self, message_router):
"""Тест регистрации обработчика callback"""
async def handler(update, context):
pass
message_router.register_callback_handler(
'test_callback', handler, UserRole.USER, 'Test callback handler'
)
assert 'test_callback' in message_router.callback_handlers
config = message_router.callback_handlers['test_callback']
assert config.required_role == UserRole.USER
assert config.description == 'Test callback handler'
def test_register_media_handler(self, message_router):
"""Тест регистрации обработчика медиа"""
async def handler(update, context):
pass
message_router.register_media_handler(
'photo', handler, UserRole.USER, 'Photo handler'
)
assert len(message_router.photo_handlers) == 1
config = message_router.photo_handlers[0]
assert config.required_role == UserRole.USER
assert config.description == 'Photo handler'
def test_register_voice_handler(self, message_router):
"""Тест регистрации обработчика голосовых сообщений"""
async def handler(update, context):
pass
message_router.register_media_handler(
'voice', handler, UserRole.USER, 'Voice handler'
)
assert len(message_router.voice_handlers) == 1
config = message_router.voice_handlers[0]
assert config.required_role == UserRole.USER
@pytest.mark.asyncio
async def test_route_text_message_no_match(self, message_router, mock_update_text, mock_context):
"""Тест маршрутизации текста без совпадений"""
result = await message_router.route_text_message(mock_update_text, mock_context)
assert result == False
@pytest.mark.asyncio
async def test_route_text_message_with_match(self, message_router, mock_update_text, mock_context):
"""Тест маршрутизации текста с совпадением"""
handler_called = False
async def test_handler(update, context):
nonlocal handler_called
handler_called = True
message_router.register_text_handler(r'hello', test_handler, UserRole.USER)
result = await message_router.route_text_message(mock_update_text, mock_context)
# Проверяем, что обработчик был вызван
assert handler_called == True
assert result == True
@pytest.mark.asyncio
async def test_route_text_message_insufficient_permissions(self, message_router, mock_update_text, mock_context):
"""Тест маршрутизации текста при недостаточных правах"""
handler_called = False
async def test_handler(update, context):
nonlocal handler_called
handler_called = True
# Регистрируем обработчик требующий админские права
message_router.register_text_handler(r'hello', test_handler, UserRole.ADMIN)
result = await message_router.route_text_message(mock_update_text, mock_context)
assert result == False
assert handler_called == False
@pytest.mark.asyncio
async def test_route_callback_exact_match(self, message_router, mock_update_callback, mock_context):
"""Тест маршрутизации callback с точным совпадением"""
handler_called = False
async def test_handler(update, context):
nonlocal handler_called
handler_called = True
message_router.register_callback_handler('test_callback', test_handler, UserRole.USER)
result = await message_router.route_callback(mock_update_callback, mock_context)
# Проверяем, что обработчик был вызван
assert handler_called == True
assert result == True
@pytest.mark.asyncio
async def test_route_callback_prefix_match(self, message_router, mock_update_callback, mock_context):
"""Тест маршрутизации callback по префиксу"""
handler_called = False
async def test_handler(update, context):
nonlocal handler_called
handler_called = True
message_router.register_callback_handler('test_', test_handler, UserRole.USER)
result = await message_router.route_callback(mock_update_callback, mock_context)
# Проверяем, что обработчик был вызван
assert handler_called == True
assert result == True
@pytest.mark.asyncio
async def test_route_callback_no_match(self, message_router, mock_update_callback, mock_context):
"""Тест маршрутизации callback без совпадений"""
result = await message_router.route_callback(mock_update_callback, mock_context)
assert result == False
@pytest.mark.asyncio
async def test_route_voice_message(self, message_router, mock_update_voice, mock_context):
"""Тест маршрутизации голосовых сообщений"""
handler_called = False
async def test_handler(update, context):
nonlocal handler_called
handler_called = True
message_router.register_media_handler('voice', test_handler, UserRole.USER)
result = await message_router.route_voice_message(mock_update_voice, mock_context)
# Проверяем, что обработчик был вызван
assert handler_called == True
assert result == True
def test_get_registered_handlers_count(self, message_router):
"""Тест получения статистики обработчиков"""
# Регистрируем несколько обработчиков
async def handler(update, context):
pass
message_router.register_text_handler(r'test', handler)
message_router.register_callback_handler('callback', handler)
message_router.register_media_handler('photo', handler)
stats = message_router.get_registered_handlers_count()
assert stats['text'] == 1
assert stats['callback'] == 1
assert stats['photo'] == 1
assert stats['voice'] == 0
def test_message_handler_config(self):
"""Тест конфигурации обработчика"""
async def handler():
pass
config = MessageHandlerConfig(handler, UserRole.MODERATOR, 5, 'Test config')
assert config.handler == handler
assert config.required_role == UserRole.MODERATOR
assert config.priority == 5
assert config.description == 'Test config'
@@ -0,0 +1,415 @@
"""
Интеграционные тесты для MessageTypeRouter с триггерами.
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
from telegram import Update, Message, User, CallbackQuery
from core.message_router import MessageTypeRouter, create_message_router
from services.trigger_service import TriggerService
class TestMessageRouterTriggers:
"""Интеграционные тесты маршрутизатора сообщений с триггерами"""
@pytest.fixture
def mock_trigger_service(self):
"""Мок сервиса триггеров"""
service = Mock(spec=TriggerService)
service.check_triggers = AsyncMock(return_value=[])
service.execute_trigger_actions = AsyncMock(return_value=[])
return service
@pytest.fixture
def message_router_with_triggers(self, mock_trigger_service):
"""Фикстура маршрутизатора с сервисом триггеров"""
return create_message_router(mock_trigger_service)
@pytest.fixture
def mock_update_group_text(self):
"""Мок-объект для текстового сообщения в группе"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
chat = Mock()
user.id = 123456
message.text = "hello world"
message.voice = None
message.photo = None
message.message_id = 789
message.chat_id = 987654
chat.type = "group"
update.message = message
update.effective_user = user
update.effective_chat = chat
return update
@pytest.fixture
def mock_update_private_text(self):
"""Мок-объект для текстового сообщения в привате"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
chat = Mock()
user.id = 123456
message.text = "private message"
message.voice = None
message.photo = None
chat.type = "private"
update.message = message
update.effective_user = user
update.effective_chat = chat
return update
@pytest.fixture
def mock_context(self):
"""Мок-объект для контекста"""
context = Mock()
context.bot = Mock()
context.bot.send_message = AsyncMock()
context.bot.send_sticker = AsyncMock()
context.bot.send_animation = AsyncMock()
context.bot.set_message_reaction = AsyncMock()
return context
@pytest.mark.asyncio
async def test_route_text_message_no_triggers_group(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест маршрутизации текста в группе без сработавших триггеров"""
result = await message_router_with_triggers.route_text_message(
mock_update_group_text, mock_context
)
# Проверяем, что проверка триггеров была вызвана для группы
message_router_with_triggers.trigger_service.check_triggers.assert_called_once_with(
"hello world", "group"
)
# Проверяем, что выполнение действий не было вызвано
message_router_with_triggers.trigger_service.execute_trigger_actions.assert_not_called()
assert result == False # Нет совпадений с текстовыми обработчиками
@pytest.mark.asyncio
async def test_route_text_message_no_triggers_private(self, message_router_with_triggers,
mock_update_private_text, mock_context):
"""Тест маршрутизации текста в привате без сработавших триггеров"""
result = await message_router_with_triggers.route_text_message(
mock_update_private_text, mock_context
)
# Проверяем, что проверка триггеров НЕ была вызвана для приватного чата
message_router_with_triggers.trigger_service.check_triggers.assert_not_called()
# Проверяем, что выполнение действий не было вызвано
message_router_with_triggers.trigger_service.execute_trigger_actions.assert_not_called()
assert result == False
@pytest.mark.asyncio
async def test_route_text_message_with_trigger_match_group(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест маршрутизации текста в группе с сработавшим триггером"""
# Настраиваем мок триггера
matched_triggers = [
{
'id': 1,
'name': 'hello_trigger',
'response_text': 'Hi there!',
'trigger_count': 5
}
]
message_router_with_triggers.trigger_service.check_triggers.return_value = matched_triggers
message_router_with_triggers.trigger_service.execute_trigger_actions.return_value = [
{
'type': 'text',
'content': 'Hi there!',
'trigger_id': 1,
'trigger_name': 'hello_trigger'
}
]
result = await message_router_with_triggers.route_text_message(
mock_update_group_text, mock_context
)
# Проверяем, что проверка триггеров была вызвана
message_router_with_triggers.trigger_service.check_triggers.assert_called_once_with(
"hello world", "group"
)
# Проверяем, что выполнение действий было вызвано
message_router_with_triggers.trigger_service.execute_trigger_actions.assert_called_once_with(
matched_triggers, 987654, "hello world", 789
)
assert result == False # Нет совпадений с текстовыми обработчиками маршрутизатора
@pytest.mark.asyncio
async def test_trigger_actions_execution_text(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения действий триггера - текстовый ответ"""
actions = [
{
'type': 'text',
'content': 'Hello from trigger!',
'trigger_id': 1,
'trigger_name': 'test_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что сообщение было отправлено
mock_context.bot.send_message.assert_called_once_with(
chat_id=987654,
text='Hello from trigger!',
reply_to_message_id=789
)
@pytest.mark.asyncio
async def test_trigger_actions_execution_sticker(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения действий триггера - стикер"""
actions = [
{
'type': 'sticker',
'content': 'sticker_file_id',
'trigger_id': 1,
'trigger_name': 'sticker_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что стикер был отправлен
mock_context.bot.send_sticker.assert_called_once_with(
chat_id=987654,
sticker='sticker_file_id',
reply_to_message_id=789
)
@pytest.mark.asyncio
async def test_trigger_actions_execution_gif(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения действий триггера - GIF"""
actions = [
{
'type': 'gif',
'content': 'gif_file_id',
'trigger_id': 1,
'trigger_name': 'gif_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что GIF был отправлен
mock_context.bot.send_animation.assert_called_once_with(
chat_id=987654,
animation='gif_file_id',
reply_to_message_id=789
)
@pytest.mark.asyncio
async def test_trigger_actions_execution_reaction(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения действий триггера - реакция"""
actions = [
{
'type': 'reaction',
'reaction_type': 'emoji',
'content': '👍',
'message_id': 789,
'trigger_id': 1,
'trigger_name': 'reaction_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что реакция была установлена
mock_context.bot.set_message_reaction.assert_called_once_with(
chat_id=987654,
message_id=789,
reaction='👍'
)
@pytest.mark.asyncio
async def test_trigger_actions_execution_multiple_actions(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения множественных действий триггера"""
actions = [
{
'type': 'text',
'content': 'Hello!',
'trigger_id': 1,
'trigger_name': 'multi_trigger'
},
{
'type': 'sticker',
'content': 'sticker123',
'trigger_id': 1,
'trigger_name': 'multi_trigger'
},
{
'type': 'reaction',
'reaction_type': 'emoji',
'content': '❤️',
'message_id': 789,
'trigger_id': 1,
'trigger_name': 'multi_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что все действия были выполнены
assert mock_context.bot.send_message.call_count == 1
assert mock_context.bot.send_sticker.call_count == 1
assert mock_context.bot.set_message_reaction.call_count == 1
@pytest.mark.asyncio
async def test_trigger_actions_execution_reaction_priority(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест приоритета реакций - только первая реакция выполняется"""
actions = [
{
'type': 'reaction',
'reaction_type': 'emoji',
'content': '👍',
'message_id': 789,
'trigger_id': 1,
'trigger_name': 'first_reaction'
},
{
'type': 'reaction',
'reaction_type': 'emoji',
'content': '❤️',
'message_id': 789,
'trigger_id': 2,
'trigger_name': 'second_reaction'
},
{
'type': 'text',
'content': 'Text response',
'trigger_id': 1,
'trigger_name': 'text_trigger'
}
]
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что только первая реакция была установлена
mock_context.bot.set_message_reaction.assert_called_once_with(
chat_id=987654,
message_id=789,
reaction='👍'
)
# Проверяем, что текстовый ответ был отправлен
mock_context.bot.send_message.assert_called_once_with(
chat_id=987654,
text='Text response',
reply_to_message_id=789
)
@pytest.mark.asyncio
async def test_trigger_actions_execution_empty_actions(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения пустого списка действий"""
actions = []
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что ничего не было отправлено
mock_context.bot.send_message.assert_not_called()
mock_context.bot.send_sticker.assert_not_called()
mock_context.bot.send_animation.assert_not_called()
mock_context.bot.set_message_reaction.assert_not_called()
@pytest.mark.asyncio
async def test_trigger_actions_execution_unknown_action_type(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест выполнения действия с неизвестным типом"""
actions = [
{
'type': 'unknown',
'content': 'some_content',
'trigger_id': 1,
'trigger_name': 'unknown_trigger'
}
]
# Не должно выбрасывать исключение, просто игнорирует неизвестный тип
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что ничего не было отправлено
mock_context.bot.send_message.assert_not_called()
mock_context.bot.send_sticker.assert_not_called()
mock_context.bot.send_animation.assert_not_called()
mock_context.bot.set_message_reaction.assert_not_called()
@pytest.mark.asyncio
async def test_route_text_message_trigger_service_error(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест обработки ошибки в сервисе триггеров"""
message_router_with_triggers.trigger_service.check_triggers.side_effect = Exception("Test error")
# Не должно выбрасывать исключение
result = await message_router_with_triggers.route_text_message(
mock_update_group_text, mock_context
)
assert result == False
@pytest.mark.asyncio
async def test_execute_trigger_actions_error_handling(self, message_router_with_triggers,
mock_update_group_text, mock_context):
"""Тест обработки ошибок при выполнении действий триггера"""
actions = [
{
'type': 'text',
'content': 'Hello!',
'trigger_id': 1,
'trigger_name': 'error_trigger'
}
]
# Настраиваем ошибку при отправке сообщения
mock_context.bot.send_message.side_effect = Exception("Send error")
# Не должно выбрасывать исключение наружу
await message_router_with_triggers._execute_trigger_actions(actions, mock_update_group_text, mock_context)
# Проверяем, что попытка отправки была
mock_context.bot.send_message.assert_called_once()
def test_set_trigger_service(self, message_router_with_triggers, mock_trigger_service):
"""Тест установки сервиса триггеров"""
new_trigger_service = Mock(spec=TriggerService)
message_router_with_triggers.set_trigger_service(new_trigger_service)
assert message_router_with_triggers.trigger_service == new_trigger_service
def test_create_message_router_with_trigger_service(self, mock_trigger_service):
"""Тест создания маршрутизатора с сервисом триггеров"""
router = create_message_router(mock_trigger_service)
assert isinstance(router, MessageTypeRouter)
assert router.trigger_service == mock_trigger_service
def test_create_message_router_without_trigger_service(self):
"""Тест создания маршрутизатора без сервиса триггеров"""
router = create_message_router()
assert isinstance(router, MessageTypeRouter)
assert router.trigger_service is None
+366
View File
@@ -0,0 +1,366 @@
"""
Тесты для модуля безопасности платежей.
"""
import pytest
import hmac
import hashlib
import subprocess
from unittest.mock import patch
from core.payment_security import PaymentSecurity, PaymentRateLimiter
class TestPaymentSecurity:
"""Тесты для класса PaymentSecurity."""
def test_validate_stripe_signature_valid(self):
"""Тест валидной подписи Stripe."""
payload = b'test_payload'
secret = 'test_secret'
timestamp = '1234567890'
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
signature = f"t={timestamp},v1={expected_signature}"
assert PaymentSecurity.validate_stripe_signature(payload, signature, secret)
def test_validate_stripe_signature_invalid(self):
"""Тест невалидной подписи Stripe."""
payload = b'test_payload'
secret = 'test_secret'
signature = "t=123456,v1=invalid_signature"
assert not PaymentSecurity.validate_stripe_signature(payload, signature, secret)
def test_validate_yookassa_signature_valid(self):
"""Тест валидной подписи YooKassa."""
request_body = 'test_body'
secret = 'test_secret'
expected_signature = hmac.new(
secret.encode('utf-8'),
request_body.encode('utf-8'),
hashlib.sha256
).hexdigest()
assert PaymentSecurity.validate_yookassa_signature(request_body, expected_signature, secret)
def test_validate_yookassa_signature_invalid(self):
"""Тест невалидной подписи YooKassa."""
request_body = 'test_body'
secret = 'test_secret'
signature = 'invalid_signature'
assert not PaymentSecurity.validate_yookassa_signature(request_body, signature, secret)
def test_validate_sbp_signature_valid(self):
"""Тест валидной подписи СБП."""
request_body = 'test_body'
secret = 'test_secret'
# СБП использует base64-encoded HMAC-SHA256
expected_hmac = hmac.new(
secret.encode('utf-8'),
request_body.encode('utf-8'),
hashlib.sha256
)
import base64
expected_signature = base64.b64encode(expected_hmac.digest()).decode('utf-8')
assert PaymentSecurity.validate_sbp_signature(request_body, expected_signature, secret)
def test_validate_sbp_signature_invalid(self):
"""Тест невалидной подписи СБП."""
request_body = 'test_body'
secret = 'test_secret'
signature = 'invalid_signature'
assert not PaymentSecurity.validate_sbp_signature(request_body, signature, secret)
@patch('core.payment_security.datetime')
def test_generate_idempotency_key(self, mock_datetime):
"""Тест генерации ключа идемпотентности."""
# Мокаем datetime для предсказуемого результата
mock_datetime.now.return_value.timestamp.return_value = 1234567890.0
payment_data = {
'user_id': 123,
'amount': 100.0
}
key1 = PaymentSecurity.generate_idempotency_key(payment_data)
# Изменяем timestamp
mock_datetime.now.return_value.timestamp.return_value = 1234567891.0
key2 = PaymentSecurity.generate_idempotency_key(payment_data)
# Ключи должны быть разными из-за timestamp
assert key1 != key2
assert len(key1) == 16
assert len(key2) == 16
def test_sanitize_payment_data(self):
"""Тест очистки платежных данных."""
payment_data = {
'user_id': 123,
'amount': 100.0,
'card_number': '1234567890123456',
'cvv': '123',
'cardholder_name': 'John Doe',
'api_key': 'secret_key',
'normal_field': 'normal_value'
}
sanitized = PaymentSecurity.sanitize_payment_data(payment_data)
assert sanitized['card_number'] == '**** **** **** 3456'
assert sanitized['cvv'] == '***'
assert sanitized['cardholder_name'] == '***'
assert sanitized['api_key'] == '***'
assert sanitized['user_id'] == 123
assert sanitized['amount'] == 100.0
assert sanitized['normal_field'] == 'normal_value'
def test_validate_amount_range(self):
"""Тест проверки диапазона суммы."""
assert PaymentSecurity.validate_amount_range(100.0)
assert PaymentSecurity.validate_amount_range(1.0)
assert PaymentSecurity.validate_amount_range(100000.0)
assert not PaymentSecurity.validate_amount_range(0.5)
assert not PaymentSecurity.validate_amount_range(200000.0)
def test_validate_currency(self):
"""Тест проверки валюты."""
assert PaymentSecurity.validate_currency('RUB')
assert PaymentSecurity.validate_currency('USD')
assert PaymentSecurity.validate_currency('EUR')
assert not PaymentSecurity.validate_currency('BTC')
assert PaymentSecurity.validate_currency('rub', ['RUB', 'USD'])
def test_detect_suspicious_activity(self):
"""Тест обнаружения подозрительной активности."""
payment_data = {'amount': 100.0}
recent_payments = []
result = PaymentSecurity.detect_suspicious_activity(payment_data, recent_payments)
assert not result['is_suspicious']
# Тест с круглой суммой
payment_data_round = {'amount': 5000.0}
result_round = PaymentSecurity.detect_suspicious_activity(payment_data_round, recent_payments)
assert result_round['is_suspicious']
assert 'round_amount_suspicious' in result_round['reasons']
def test_encrypt_sensitive_data_success(self):
"""Тест успешного шифрования данных."""
data = 'test_sensitive_data'
key = 'test_encryption_key_32_chars_long'
encrypted = PaymentSecurity.encrypt_sensitive_data(data, key)
assert encrypted != data
assert isinstance(encrypted, str)
# Проверяем, что можем расшифровать обратно
decrypted = PaymentSecurity.decrypt_sensitive_data(encrypted, key)
assert decrypted == data
def test_decrypt_sensitive_data_success(self):
"""Тест успешной расшифровки данных."""
original_data = 'another_test_data'
key = 'another_test_key_32_chars_long'
# Шифруем и расшифровываем
encrypted = PaymentSecurity.encrypt_sensitive_data(original_data, key)
decrypted = PaymentSecurity.decrypt_sensitive_data(encrypted, key)
assert decrypted == original_data
def test_encrypt_decrypt_with_different_keys(self):
"""Тест шифрования с разными ключами."""
data = 'test_data'
key1 = 'key_one_12345678901234567890123456789012'
key2 = 'key_two_12345678901234567890123456789012'
encrypted1 = PaymentSecurity.encrypt_sensitive_data(data, key1)
encrypted2 = PaymentSecurity.encrypt_sensitive_data(data, key2)
# Шифртексты должны быть разными
assert encrypted1 != encrypted2
# Расшифровка с правильными ключами
assert PaymentSecurity.decrypt_sensitive_data(encrypted1, key1) == data
assert PaymentSecurity.decrypt_sensitive_data(encrypted2, key2) == data
def test_encrypt_with_short_key(self):
"""Тест шифрования с коротким ключом (должен работать через SHA256)."""
data = 'test_data'
short_key = 'short_key'
encrypted = PaymentSecurity.encrypt_sensitive_data(data, short_key)
decrypted = PaymentSecurity.decrypt_sensitive_data(encrypted, short_key)
assert decrypted == data
@patch('core.payment_security.Fernet')
def test_encrypt_without_cryptography(self, mock_fernet):
"""Тест поведения при отсутствии библиотеки cryptography."""
# Имитируем отсутствие библиотеки
original_import = __import__
def mock_import(name, *args, **kwargs):
if name == 'cryptography.fernet':
raise ImportError("No module named 'cryptography'")
return original_import(name, *args, **kwargs)
with patch('builtins.__import__', side_effect=mock_import):
data = 'test_data'
key = 'test_key'
with pytest.raises(Exception): # PaymentSecurityError
PaymentSecurity.encrypt_sensitive_data(data, key)
class TestFFmpegSecurity:
"""Тесты для проверок безопасности ffmpeg."""
def test_validate_ffmpeg_command_valid(self):
"""Тест валидной команды ffmpeg."""
command = ['ffmpeg', '-i', 'input.mp4', '-c:v', 'libx264', '-y', 'output.mp4']
assert PaymentSecurity.validate_ffmpeg_command(command)
def test_validate_ffmpeg_command_invalid_not_ffmpeg(self):
"""Тест команды не начинающейся с ffmpeg."""
command = ['convert', '-i', 'input.mp4', 'output.mp4']
assert not PaymentSecurity.validate_ffmpeg_command(command)
def test_validate_ffmpeg_command_dangerous_patterns(self):
"""Тест обнаружения опасных паттернов."""
dangerous_commands = [
['ffmpeg', '-i', '../../etc/passwd', 'output.mp4'],
['ffmpeg', '-i', 'input.mp4', '-y', '| rm -rf /'],
['ffmpeg', '-i', 'input.mp4; rm -rf /', 'output.mp4'],
['ffmpeg', '-i', 'input.mp4', '-f', 'srt', 'output.srt'],
]
for command in dangerous_commands:
assert not PaymentSecurity.validate_ffmpeg_command(command)
def test_validate_ffmpeg_command_unsupported_format(self):
"""Тест неподдерживаемого формата файла."""
command = ['ffmpeg', '-i', 'input.exe', 'output.mp4']
assert not PaymentSecurity.validate_ffmpeg_command(command)
def test_sanitize_ffmpeg_output_path_valid(self):
"""Тест санитизации валидного пути."""
path = 'output.mp4'
assert PaymentSecurity.sanitize_ffmpeg_output_path(path) == path
def test_sanitize_ffmpeg_output_path_invalid(self):
"""Тест санитизации невалидного пути."""
invalid_paths = [
'../../etc/passwd',
'/etc/passwd',
'output.mp4|rm -rf /',
'output.exe',
'output; rm -rf /',
]
for path in invalid_paths:
assert PaymentSecurity.sanitize_ffmpeg_output_path(path) is None
def test_secure_ffmpeg_execute_success(self):
"""Тест успешного выполнения безопасной команды ffmpeg."""
# Мокаем subprocess.run для тестирования
with patch('subprocess.run') as mock_run:
mock_run.return_value = type('MockResult', (), {'returncode': 0, 'stdout': '', 'stderr': ''})()
command = ['ffmpeg', '-i', 'input.mp4', '-c:v', 'libx264', '-y', 'output.mp4']
success, error = PaymentSecurity.secure_ffmpeg_execute(command)
assert success
assert error == ""
mock_run.assert_called_once()
def test_secure_ffmpeg_execute_validation_failure(self):
"""Тест выполнения с проваленной валидацией."""
command = ['not_ffmpeg', 'bad_command'] # Не пройдет валидацию
success, error = PaymentSecurity.secure_ffmpeg_execute(command)
assert not success
assert "validation failed" in error.lower()
def test_secure_ffmpeg_execute_subprocess_error(self):
"""Тест выполнения с ошибкой subprocess."""
with patch('subprocess.run') as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired(cmd=['ffmpeg'], timeout=300)
command = ['ffmpeg', '-i', 'input.mp4', '-y', 'output.mp4']
success, error = PaymentSecurity.secure_ffmpeg_execute(command, timeout_seconds=300)
assert not success
assert "timed out" in error.lower()
class TestPaymentRateLimiter:
"""Тесты для ограничителя частоты платежей."""
def test_init(self):
"""Тест инициализации ограничителя."""
limiter = PaymentRateLimiter(max_payments_per_hour=5, max_amount_per_hour=10000)
assert limiter.max_payments_per_hour == 5
assert limiter.max_amount_per_hour == 10000
def test_check_rate_limit_allowed(self):
"""Тест проверки лимита для разрешенного платежа."""
limiter = PaymentRateLimiter(max_payments_per_hour=2, max_amount_per_hour=1000)
user_id = 123
amount = 100.0
result = limiter.check_rate_limit(user_id, amount)
assert result['allowed']
assert result['remaining_payments'] == 2
assert result['remaining_amount'] == 900.0
def test_check_rate_limit_exceeded_payments(self):
"""Тест превышения лимита по количеству платежей."""
limiter = PaymentRateLimiter(max_payments_per_hour=2, max_amount_per_hour=1000)
user_id = 123
# Записываем первый платеж
limiter.record_payment(user_id, 100.0)
# Записываем второй платеж
limiter.record_payment(user_id, 100.0)
# Проверяем лимит для третьего платежа - должен быть превышен
result = limiter.check_rate_limit(user_id, 100.0)
assert not result['allowed']
def test_check_rate_limit_exceeded_amount(self):
"""Тест превышения лимита по сумме."""
limiter = PaymentRateLimiter(max_payments_per_hour=10, max_amount_per_hour=500)
user_id = 123
# Большой платеж
result = limiter.check_rate_limit(user_id, 600.0)
assert not result['allowed']
def test_record_payment(self):
"""Тест записи платежа."""
limiter = PaymentRateLimiter()
user_id = 123
amount = 100.0
limiter.record_payment(user_id, amount)
assert user_id in limiter._counters
assert len(limiter._counters[user_id]['payments']) == 1
assert len(limiter._counters[user_id]['amounts']) == 1
assert limiter._counters[user_id]['amounts'][0][1] == amount
+220
View File
@@ -0,0 +1,220 @@
"""
Тесты для системы разрешений и ролей.
Проверяет корректность работы PermissionManager.
"""
import sys
import os
import pytest
from unittest.mock import Mock, AsyncMock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.permissions import PermissionManager, UserRole, Permission
class TestPermissionManager:
"""Тесты менеджера разрешений"""
@pytest.fixture
def permission_manager(self):
"""Экземпляр менеджера разрешений"""
return PermissionManager()
@pytest.fixture
def mock_config(self):
"""Мок конфигурации"""
config = Mock()
config.bot_config = Mock()
config.bot_config.super_admin_ids = [111111111, 222222222]
config.bot_config.admin_ids = [333333333, 444444444]
config.bot_config.moderator_ids = [555555555, 666666666]
return config
@pytest.fixture
def mock_update(self):
"""Мок обновления"""
update = Mock()
update.effective_chat = Mock()
return update
def test_get_role_permissions_user(self, permission_manager):
"""Тест получения разрешений для роли USER"""
permissions = permission_manager.get_role_permissions(UserRole.USER)
expected_permissions = {
Permission.USE_BASIC_COMMANDS,
Permission.USE_GAMES,
Permission.USE_INFO,
Permission.USE_DONATE,
}
assert permissions == expected_permissions
def test_get_role_permissions_admin(self, permission_manager):
"""Тест получения разрешений для роли ADMIN"""
permissions = permission_manager.get_role_permissions(UserRole.ADMIN)
assert Permission.BAN_USERS in permissions
assert Permission.VIEW_ADMIN_STATS in permissions
assert len(permissions) > 10 # Должно быть много разрешений
def test_get_role_permissions_super_admin(self, permission_manager):
"""Тест получения разрешений для роли SUPER_ADMIN"""
permissions = permission_manager.get_role_permissions(UserRole.SUPER_ADMIN)
# Супер-администратор должен иметь все разрешения
assert len(permissions) == len(list(Permission))
assert Permission.MANAGE_SYSTEM in permissions
assert Permission.MANAGE_ADMINS in permissions
def test_has_permission_user(self, permission_manager):
"""Тест проверки наличия разрешения у роли USER"""
assert permission_manager.has_permission(UserRole.USER, Permission.USE_BASIC_COMMANDS)
assert permission_manager.has_permission(UserRole.USER, Permission.USE_GAMES)
assert not permission_manager.has_permission(UserRole.USER, Permission.BAN_USERS)
def test_has_permission_admin(self, permission_manager):
"""Тест проверки наличия разрешения у роли ADMIN"""
assert permission_manager.has_permission(UserRole.ADMIN, Permission.BAN_USERS)
assert permission_manager.has_permission(UserRole.ADMIN, Permission.USE_BASIC_COMMANDS)
assert not permission_manager.has_permission(UserRole.ADMIN, Permission.MANAGE_SYSTEM)
def test_has_permission_super_admin(self, permission_manager):
"""Тест проверки наличия разрешения у роли SUPER_ADMIN"""
# Супер-администратор должен иметь все разрешения
for permission in Permission:
assert permission_manager.has_permission(UserRole.SUPER_ADMIN, permission)
def test_get_user_role_from_config_super_admin(self, permission_manager, mock_config):
"""Тест определения роли супер-администратора из конфигурации"""
role = permission_manager.get_user_role_from_config(111111111, mock_config)
assert role == UserRole.SUPER_ADMIN
def test_get_user_role_from_config_admin(self, permission_manager, mock_config):
"""Тест определения роли администратора из конфигурации"""
role = permission_manager.get_user_role_from_config(333333333, mock_config)
assert role == UserRole.ADMIN
def test_get_user_role_from_config_moderator(self, permission_manager, mock_config):
"""Тест определения роли модератора из конфигурации"""
role = permission_manager.get_user_role_from_config(555555555, mock_config)
assert role == UserRole.MODERATOR
def test_get_user_role_from_config_user(self, permission_manager, mock_config):
"""Тест определения роли обычного пользователя из конфигурации"""
role = permission_manager.get_user_role_from_config(777777777, mock_config)
assert role == UserRole.USER
@pytest.mark.asyncio
async def test_get_user_role_from_chat_creator(self, permission_manager, mock_update):
"""Тест определения роли создателя чата"""
# Мокируем создателя чата
mock_member = Mock()
mock_member.status = 'creator'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_user_role_from_chat(mock_update, 123456789)
assert role == UserRole.SUPER_ADMIN
@pytest.mark.asyncio
async def test_get_user_role_from_chat_administrator(self, permission_manager, mock_update):
"""Тест определения роли администратора чата"""
# Мокируем администратора чата
mock_member = Mock()
mock_member.status = 'administrator'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_user_role_from_chat(mock_update, 123456789)
assert role == UserRole.ADMIN
@pytest.mark.asyncio
async def test_get_user_role_from_chat_user(self, permission_manager, mock_update):
"""Тест определения роли обычного пользователя в чате"""
# Мокируем обычного пользователя
mock_member = Mock()
mock_member.status = 'member'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_user_role_from_chat(mock_update, 123456789)
assert role == UserRole.USER
@pytest.mark.asyncio
async def test_get_user_role_from_chat_error(self, permission_manager, mock_update):
"""Тест обработки ошибки при получении роли из чата"""
# Мокируем ошибку при получении члена
mock_update.effective_chat.get_member = AsyncMock(side_effect=Exception("API Error"))
role = await permission_manager.get_user_role_from_chat(mock_update, 123456789)
assert role == UserRole.USER # Должна вернуться роль по умолчанию
@pytest.mark.asyncio
async def test_get_effective_role_config_priority(self, permission_manager, mock_config, mock_update):
"""Тест получения эффективной роли с приоритетом конфигурации"""
# Пользователь является супер-администратором в конфигурации
# и обычным пользователем в чате
mock_member = Mock()
mock_member.status = 'member'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_effective_role(mock_update, 111111111, mock_config)
assert role == UserRole.SUPER_ADMIN # Должна быть выбрана роль из конфигурации
@pytest.mark.asyncio
async def test_get_effective_role_chat_priority(self, permission_manager, mock_config, mock_update):
"""Тест получения эффективной роли с приоритетом чата"""
# Пользователь является администратором в чате
# и обычным пользователем в конфигурации
mock_member = Mock()
mock_member.status = 'administrator'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_effective_role(mock_update, 777777777, mock_config)
assert role == UserRole.ADMIN # Должна быть выбрана роль из чата
@pytest.mark.asyncio
async def test_get_effective_role_both_user(self, permission_manager, mock_config, mock_update):
"""Тест получения эффективной роли когда обе роли - USER"""
# Пользователь является обычным пользователем везде
mock_member = Mock()
mock_member.status = 'member'
mock_update.effective_chat.get_member = AsyncMock(return_value=mock_member)
role = await permission_manager.get_effective_role(mock_update, 777777777, mock_config)
assert role == UserRole.USER
def test_get_required_permissions_for_command_basic(self, permission_manager):
"""Тест получения необходимых разрешений для базовой команды"""
permissions = permission_manager.get_required_permissions_for_command('start')
assert permissions == {Permission.USE_BASIC_COMMANDS}
def test_get_required_permissions_for_command_game(self, permission_manager):
"""Тест получения необходимых разрешений для игровой команды"""
permissions = permission_manager.get_required_permissions_for_command('play_game')
assert permissions == {Permission.USE_GAMES}
def test_get_required_permissions_for_command_admin(self, permission_manager):
"""Тест получения необходимых разрешений для административной команды"""
permissions = permission_manager.get_required_permissions_for_command('ban')
assert permissions == {Permission.BAN_USERS, Permission.VIEW_ADMIN_STATS}
def test_get_required_permissions_for_command_unknown(self, permission_manager):
"""Тест получения необходимых разрешений для неизвестной команды"""
permissions = permission_manager.get_required_permissions_for_command('unknown_command')
assert permissions == {Permission.USE_BASIC_COMMANDS} # По умолчанию
def test_can_execute_command_user_allowed(self, permission_manager):
"""Тест проверки возможности выполнения команды для разрешенной роли"""
can_execute = permission_manager.can_execute_command(UserRole.USER, 'start')
assert can_execute is True
def test_can_execute_command_user_denied(self, permission_manager):
"""Тест проверки возможности выполнения команды для запрещенной роли"""
can_execute = permission_manager.can_execute_command(UserRole.USER, 'ban')
assert can_execute is False
def test_can_execute_command_admin_allowed(self, permission_manager):
"""Тест проверки возможности выполнения команды для администратора"""
can_execute = permission_manager.can_execute_command(UserRole.ADMIN, 'ban')
assert can_execute is True
def test_can_execute_command_super_admin_allowed(self, permission_manager):
"""Тест проверки возможности выполнения команды для супер-администратора"""
can_execute = permission_manager.can_execute_command(UserRole.SUPER_ADMIN, 'ban')
assert can_execute is True
+169
View File
@@ -0,0 +1,169 @@
"""
Тесты для UnifiedMessageRouter - интеграция всех компонентов.
"""
import pytest
from unittest.mock import Mock, AsyncMock
from telegram import Update, Message, User, CallbackQuery
from core.unified_router import UnifiedMessageRouter
from core.permissions import UserRole
class TestUnifiedMessageRouter:
"""Тесты объединенного маршрутизатора"""
@pytest.fixture
def mock_command_router(self):
"""Мок-объект для CommandRouter"""
router = Mock()
router.handle_command = AsyncMock()
return router
@pytest.fixture
def mock_message_router(self):
"""Мок-объект для MessageTypeRouter"""
router = Mock()
router.route_text_message = AsyncMock(return_value=True)
router.route_callback = AsyncMock(return_value=True)
router.route_voice_message = AsyncMock(return_value=True)
return router
@pytest.fixture
def mock_menu_manager(self):
"""Мок-объект для ContextMenuManager"""
manager = Mock()
manager.is_menu_available = Mock(return_value=True)
manager.get_menu_for_user = AsyncMock(return_value=Mock())
return manager
@pytest.fixture
def unified_router(self, mock_command_router, mock_message_router, mock_menu_manager):
"""Фикстура для UnifiedMessageRouter"""
return UnifiedMessageRouter(
mock_command_router,
mock_message_router,
mock_menu_manager
)
@pytest.fixture
def mock_update_message(self):
"""Мок-объект для обновления с сообщением"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
user.id = 123456789
message.text = "/start"
update.message = message
update.effective_user = user
update.callback_query = None
return update
@pytest.fixture
def mock_update_callback(self):
"""Мок-объект для обновления с callback"""
update = Mock(spec=Update)
callback_query = Mock(spec=CallbackQuery)
user = Mock(spec=User)
user.id = 123456789
callback_query.data = "menu_main"
update.callback_query = callback_query
update.effective_user = user
update.message = None
return update
@pytest.fixture
def mock_context(self):
"""Мок-объект для контекста"""
return Mock()
@pytest.mark.asyncio
async def test_handle_message_command(self, unified_router, mock_update_message, mock_context, mock_command_router):
"""Тест обработки команд"""
mock_update_message.message.text = "/start"
result = await unified_router.handle_update(mock_update_message, mock_context)
assert result == True
mock_command_router.handle_command.assert_called_once()
@pytest.mark.asyncio
async def test_handle_message_text(self, unified_router, mock_update_message, mock_context, mock_message_router):
"""Тест обработки текстовых сообщений"""
mock_update_message.message.text = "Hello world"
result = await unified_router.handle_update(mock_update_message, mock_context)
assert result == True
mock_message_router.route_text_message.assert_called_once()
@pytest.mark.asyncio
async def test_handle_callback_menu_allowed(self, unified_router, mock_update_callback, mock_context,
mock_menu_manager, mock_message_router):
"""Тест обработки callback с разрешенным меню"""
mock_menu_manager.is_menu_available.return_value = True
result = await unified_router.handle_update(mock_update_callback, mock_context)
assert result == True
mock_menu_manager.is_menu_available.assert_called_once_with('menu_main', UserRole.USER)
mock_message_router.route_callback.assert_called_once()
@pytest.mark.asyncio
async def test_handle_callback_menu_denied(self, unified_router, mock_update_callback, mock_context,
mock_menu_manager):
"""Тест обработки callback с запрещенным меню"""
mock_menu_manager.is_menu_available.return_value = False
result = await unified_router.handle_update(mock_update_callback, mock_context)
assert result == True
mock_menu_manager.is_menu_available.assert_called_once()
# route_callback не должен вызываться
mock_menu_manager.route_callback.assert_not_called()
@pytest.mark.asyncio
async def test_get_menu_for_user(self, unified_router, mock_menu_manager):
"""Тест получения меню для пользователя"""
mock_menu = Mock()
mock_menu_manager.get_menu_for_user.return_value = mock_menu
result = await unified_router.get_menu_for_user('menu_main', UserRole.USER)
assert result == mock_menu
mock_menu_manager.get_menu_for_user.assert_called_once_with('menu_main', UserRole.USER)
def test_get_registered_handlers_count(self, unified_router, mock_command_router, mock_message_router):
"""Тест получения статистики обработчиков"""
mock_command_router.command_handlers = {'start': Mock(), 'help': Mock()}
mock_command_router.callback_handlers = {'menu': Mock()}
mock_message_router.get_registered_handlers_count.return_value = {
'text': 5,
'callback': 3,
'voice': 2
}
stats = unified_router.get_registered_handlers_count()
expected = {
'command_handlers': 2,
'callback_handlers': 1,
'message_handlers': {
'text': 5,
'callback': 3,
'voice': 2
},
'total': 2 + 1 + 5 + 3 + 2
}
assert stats == expected
def test_clear_menu_cache(self, unified_router, mock_menu_manager):
"""Тест очистки кеша меню"""
unified_router.clear_menu_cache()
mock_menu_manager.clear_cache.assert_called_once()