mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# Тесты для Telegram Bot
|
||||
|
||||
Этот каталог содержит тесты для различных компонентов телеграм-бота.
|
||||
|
||||
## Структура тестов
|
||||
|
||||
- `test_services/` - Unit-тесты для сервисов (user_service, game_service)
|
||||
- `test_utils/` - Тесты для утилит (formatters, helpers, validators, exceptions)
|
||||
- `test_integration/` - Интеграционные тесты для полного потока
|
||||
- `test_performance/` - Тесты производительности критических функций
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
### Unit-тесты сервисов
|
||||
```bash
|
||||
python -m pytest test_services/test_game_service.py -v
|
||||
python -m pytest test_services/test_user_service.py -v
|
||||
```
|
||||
|
||||
### Интеграционные тесты
|
||||
```bash
|
||||
python test_games_navigation.py # Тестирование навигации к играм
|
||||
python -m pytest test_integration/ -v
|
||||
```
|
||||
|
||||
### Все тесты
|
||||
```bash
|
||||
python -m pytest -v
|
||||
```
|
||||
|
||||
## Важные исправления
|
||||
|
||||
### Поддержка русского ввода в Rock-Paper-Scissors
|
||||
Добавлена поддержка русского ввода в game_service.py:
|
||||
- 'камень' -> 'rock'
|
||||
- 'ножницы' -> 'scissors'
|
||||
- 'бумага' -> 'paper'
|
||||
|
||||
Тесты в test_game_service.py проверяют эту функциональность.
|
||||
|
||||
### Исправление ошибок в тестах
|
||||
- Исправлен test_games_navigation.py: добавлен game_service в GameHandlers
|
||||
- Добавлены AsyncMock для Telegram API вызовов
|
||||
- Исправлены проблемы с валидацией и очисткой сессий
|
||||
|
||||
## Автоматическое тестирование
|
||||
|
||||
Тесты интегрированы в CI/CD пайплайн через GitHub Actions (.github/workflows/ci.yml).
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Тестовый пакет для телеграм-бота.
|
||||
Содержит все тесты для проверки корректности работы системы.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Добавляем корневую директорию в путь для импортов
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Общие тестовые утилиты и фикстуры
|
||||
from .conftest import *
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Конфигурация pytest для телеграм-бота.
|
||||
Содержит фикстуры и вспомогательные функции для тестирования.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from datetime import datetime
|
||||
from telegram import Update, User, Message, Chat, CallbackQuery
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import BotException, ValidationError, DatabaseError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_config():
|
||||
"""Фикстура с тестовой конфигурацией"""
|
||||
# Создаем конфигурацию с пустыми списками админов для чистых тестов
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
||||
f.write("""
|
||||
BOT_TOKEN = "123456789:test_token_for_testing"
|
||||
ADMIN_IDS = []
|
||||
SUPER_ADMIN_IDS = []
|
||||
MODERATOR_IDS = []
|
||||
OPENWEATHER_API_KEY = "test_weather_key"
|
||||
NEWS_API_KEY = "test_news_key"
|
||||
OPENAI_API_KEY = "test_openai_key"
|
||||
""")
|
||||
temp_config_path = f.name
|
||||
|
||||
try:
|
||||
config = Config(temp_config_path)
|
||||
yield config
|
||||
finally:
|
||||
if os.path.exists(temp_config_path):
|
||||
os.unlink(temp_config_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
"""Фикстура с мок-объектом пользователя"""
|
||||
user = Mock(spec=User)
|
||||
user.id = 123456789
|
||||
user.username = "test_user"
|
||||
user.first_name = "Test"
|
||||
user.last_name = "User"
|
||||
user.is_bot = False
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_chat():
|
||||
"""Фикстура с мок-объектом чата"""
|
||||
chat = Mock(spec=Chat)
|
||||
chat.id = -1001234567890
|
||||
chat.type = "group"
|
||||
chat.title = "Test Chat"
|
||||
return chat
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_message(mock_user, mock_chat):
|
||||
"""Фикстура с мок-объектом сообщения"""
|
||||
message = Mock(spec=Message)
|
||||
message.message_id = 12345
|
||||
message.text = "/test command"
|
||||
message.from_user = mock_user
|
||||
message.chat = mock_chat
|
||||
message.date = datetime.now()
|
||||
message.reply_text = AsyncMock()
|
||||
message.set_reaction = AsyncMock()
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_update(mock_message):
|
||||
"""Фикстура с мок-объектом обновления"""
|
||||
update = Mock(spec=Update)
|
||||
update.effective_user = mock_message.from_user
|
||||
update.effective_chat = mock_message.chat
|
||||
update.message = mock_message
|
||||
update.callback_query = None
|
||||
return update
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context():
|
||||
"""Фикстура с мок-объектом контекста"""
|
||||
context = Mock(spec=ContextTypes.DEFAULT_TYPE)
|
||||
context.args = []
|
||||
context.user_data = {}
|
||||
context.chat_data = {}
|
||||
context.bot = Mock()
|
||||
context.bot.send_message = AsyncMock()
|
||||
context.application = Mock()
|
||||
context.application._date_time = datetime.now()
|
||||
return context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_callback_query(mock_user, mock_chat, mock_message):
|
||||
"""Фикстура с мок-объектом callback query"""
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
callback_query.id = "test_callback_123"
|
||||
callback_query.data = "test_data"
|
||||
callback_query.from_user = mock_user
|
||||
callback_query.message = mock_message
|
||||
callback_query.answer = AsyncMock()
|
||||
callback_query.edit_message_text = AsyncMock()
|
||||
return callback_query
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user_data():
|
||||
"""Фикстура с тестовыми данными пользователя"""
|
||||
return {
|
||||
'id': 123456789,
|
||||
'telegram_id': 123456789,
|
||||
'username': 'test_user',
|
||||
'first_name': 'Test',
|
||||
'last_name': 'User',
|
||||
'reputation': 150,
|
||||
'rank': 'Активист',
|
||||
'message_count': 45,
|
||||
'joined_date': datetime.now(),
|
||||
'last_activity': datetime.now()
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_error_data():
|
||||
"""Фикстура с тестовыми данными ошибки"""
|
||||
return {
|
||||
'id': 1,
|
||||
'admin_id': 123456789,
|
||||
'error_type': 'bug',
|
||||
'title': 'Тестовая ошибка',
|
||||
'description': 'Описание тестовой ошибки для тестирования',
|
||||
'status': 'new',
|
||||
'priority': 'medium',
|
||||
'created_at': datetime.now(),
|
||||
'updated_at': datetime.now()
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db_path():
|
||||
"""Фикстура с временным путем к базе данных"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
db_path = f.name
|
||||
|
||||
yield db_path
|
||||
|
||||
# Очистка после теста
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event_loop():
|
||||
"""Фикстура для event loop (для асинхронных тестов)"""
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
# Вспомогательные функции для тестов
|
||||
|
||||
def create_test_config(bot_token: str = "123456789:test_token", admin_ids: list = None) -> Config:
|
||||
"""Создание тестовой конфигурации"""
|
||||
if admin_ids is None:
|
||||
admin_ids = [] # По умолчанию пустой список админов для тестов
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
||||
f.write(f"""
|
||||
BOT_TOKEN = "{bot_token}"
|
||||
ADMIN_IDS = {admin_ids}
|
||||
SUPER_ADMIN_IDS = []
|
||||
MODERATOR_IDS = []
|
||||
OPENWEATHER_API_KEY = "test_weather_key"
|
||||
NEWS_API_KEY = "test_news_key"
|
||||
OPENAI_API_KEY = "test_openai_key"
|
||||
""")
|
||||
config_path = f.name
|
||||
|
||||
try:
|
||||
return Config(config_path)
|
||||
finally:
|
||||
if os.path.exists(config_path):
|
||||
os.unlink(config_path)
|
||||
|
||||
|
||||
def assert_exception_raised(func, exception_type, *args, **kwargs):
|
||||
"""Утилита для проверки исключений в тестах"""
|
||||
with pytest.raises(exception_type):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
asyncio.run(func(*args, **kwargs))
|
||||
else:
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
async def async_assert_exception_raised(func, exception_type, *args, **kwargs):
|
||||
"""Асинхронная утилита для проверки исключений"""
|
||||
with pytest.raises(exception_type):
|
||||
await func(*args, **kwargs)
|
||||
|
||||
|
||||
def create_mock_user(user_id: int = 123456789, username: str = "test_user",
|
||||
first_name: str = "Test", last_name: str = "User"):
|
||||
"""Создание мок-объекта пользователя"""
|
||||
user = Mock(spec=User)
|
||||
user.id = user_id
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.is_bot = False
|
||||
return user
|
||||
|
||||
|
||||
def create_mock_chat(chat_id: int = -1001234567890, chat_type: str = "group"):
|
||||
"""Создание мок-объекта чата"""
|
||||
chat = Mock(spec=Chat)
|
||||
chat.id = chat_id
|
||||
chat.type = chat_type
|
||||
return chat
|
||||
|
||||
|
||||
def create_mock_update(user: User = None, chat: Chat = None, text: str = "/test"):
|
||||
"""Создание мок-объекта обновления"""
|
||||
if user is None:
|
||||
user = create_mock_user()
|
||||
if chat is None:
|
||||
chat = create_mock_chat()
|
||||
|
||||
message = Mock(spec=Message)
|
||||
message.text = text
|
||||
message.from_user = user
|
||||
message.chat = chat
|
||||
|
||||
update = Mock(spec=Update)
|
||||
update.effective_user = user
|
||||
update.effective_chat = chat
|
||||
update.message = message
|
||||
|
||||
return update
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Unit-тесты для TriggerRepository.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from datetime import datetime
|
||||
from database.repository import TriggerRepository
|
||||
|
||||
|
||||
class TestTriggerRepository:
|
||||
"""Тесты репозитория триггеров"""
|
||||
|
||||
@pytest.fixture
|
||||
def trigger_repository(self):
|
||||
"""Фикстура репозитория триггеров с in-memory базой"""
|
||||
repo = TriggerRepository(':memory:')
|
||||
return repo
|
||||
|
||||
def test_get_active_triggers_group(self, trigger_repository):
|
||||
"""Тест получения активных триггеров для групповых чатов"""
|
||||
# Создаем тестовые данные
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('active_group_trigger', r'hello', 'Hi!', 1, 'group'))
|
||||
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('inactive_trigger', r'bye', 'Bye!', 0, 'group'))
|
||||
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('private_trigger', r'private', 'Private!', 1, 'private'))
|
||||
|
||||
triggers = trigger_repository.get_active_triggers('group')
|
||||
|
||||
assert len(triggers) == 1
|
||||
assert triggers[0]['name'] == 'active_group_trigger'
|
||||
assert triggers[0]['pattern'] == r'hello'
|
||||
assert triggers[0]['response_text'] == 'Hi!'
|
||||
|
||||
def test_get_active_triggers_private(self, trigger_repository):
|
||||
"""Тест получения активных триггеров для приватных чатов"""
|
||||
# Создаем тестовые данные
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('private_trigger', r'private', 'Private!', 1, 'private'))
|
||||
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('group_trigger', r'group', 'Group!', 1, 'group'))
|
||||
|
||||
triggers = trigger_repository.get_active_triggers('private')
|
||||
|
||||
assert len(triggers) == 1
|
||||
assert triggers[0]['name'] == 'private_trigger'
|
||||
assert triggers[0]['chat_type'] == 'private'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_triggers_async(self, trigger_repository):
|
||||
"""Тест асинхронного получения активных триггеров"""
|
||||
# Создаем тестовые данные
|
||||
trigger_repository._execute_query("""
|
||||
INSERT INTO triggers (name, pattern, response_text, is_active, chat_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", ('async_trigger', r'async', 'Async!', 1, 'group'))
|
||||
|
||||
triggers = await trigger_repository.get_active_triggers_async('group')
|
||||
|
||||
assert len(triggers) == 1
|
||||
assert triggers[0]['name'] == 'async_trigger'
|
||||
|
||||
def test_add_trigger_success(self, trigger_repository):
|
||||
"""Тест успешного добавления триггера"""
|
||||
trigger_data = {
|
||||
'name': 'new_trigger',
|
||||
'description': 'Test trigger',
|
||||
'pattern': r'test\s+pattern',
|
||||
'response_text': 'Test response',
|
||||
'response_sticker': 'sticker123',
|
||||
'response_gif': 'gif456',
|
||||
'created_by': 123456,
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
|
||||
assert trigger_id is not None
|
||||
assert isinstance(trigger_id, int)
|
||||
|
||||
# Проверяем, что триггер был добавлен
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger is not None
|
||||
assert trigger['name'] == 'new_trigger'
|
||||
assert trigger['description'] == 'Test trigger'
|
||||
assert trigger['pattern'] == r'test\s+pattern'
|
||||
assert trigger['response_text'] == 'Test response'
|
||||
assert trigger['response_sticker'] == 'sticker123'
|
||||
assert trigger['response_gif'] == 'gif456'
|
||||
assert trigger['created_by'] == 123456
|
||||
assert trigger['chat_type'] == 'group'
|
||||
assert trigger['is_active'] == 1
|
||||
assert trigger['trigger_count'] == 0
|
||||
|
||||
def test_add_trigger_minimal_data(self, trigger_repository):
|
||||
"""Тест добавления триггера с минимальными данными"""
|
||||
trigger_data = {
|
||||
'name': 'minimal_trigger',
|
||||
'pattern': r'minimal'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
|
||||
assert trigger_id is not None
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['name'] == 'minimal_trigger'
|
||||
assert trigger['pattern'] == r'minimal'
|
||||
assert trigger['response_text'] is None
|
||||
assert trigger['chat_type'] == 'group' # значение по умолчанию
|
||||
|
||||
def test_update_trigger_stats_success(self, trigger_repository):
|
||||
"""Тест успешного обновления статистики триггера"""
|
||||
# Создаем триггер
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'stats_trigger',
|
||||
'pattern': r'stats'
|
||||
})
|
||||
|
||||
# Обновляем статистику
|
||||
success = trigger_repository.update_trigger_stats(trigger_id)
|
||||
|
||||
assert success == True
|
||||
|
||||
# Проверяем обновление
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['trigger_count'] == 1
|
||||
assert trigger['last_triggered'] is not None
|
||||
|
||||
def test_update_trigger_stats_nonexistent(self, trigger_repository):
|
||||
"""Тест обновления статистики несуществующего триггера"""
|
||||
success = trigger_repository.update_trigger_stats(99999)
|
||||
|
||||
assert success == False
|
||||
|
||||
def test_get_trigger_by_id_exists(self, trigger_repository):
|
||||
"""Тест получения существующего триггера по ID"""
|
||||
trigger_data = {
|
||||
'name': 'test_trigger',
|
||||
'pattern': r'test',
|
||||
'response_text': 'Test response'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
|
||||
assert trigger is not None
|
||||
assert trigger['id'] == trigger_id
|
||||
assert trigger['name'] == 'test_trigger'
|
||||
assert trigger['pattern'] == r'test'
|
||||
assert trigger['response_text'] == 'Test response'
|
||||
|
||||
def test_get_trigger_by_id_not_exists(self, trigger_repository):
|
||||
"""Тест получения несуществующего триггера по ID"""
|
||||
trigger = trigger_repository.get_trigger_by_id(99999)
|
||||
|
||||
assert trigger is None
|
||||
|
||||
def test_update_trigger_success(self, trigger_repository):
|
||||
"""Тест успешного обновления триггера"""
|
||||
# Создаем триггер
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'update_trigger',
|
||||
'pattern': r'old_pattern',
|
||||
'response_text': 'Old response'
|
||||
})
|
||||
|
||||
# Обновляем
|
||||
update_data = {
|
||||
'name': 'updated_trigger',
|
||||
'pattern': r'new_pattern',
|
||||
'response_text': 'New response'
|
||||
}
|
||||
|
||||
success = trigger_repository.update_trigger(trigger_id, update_data)
|
||||
|
||||
assert success == True
|
||||
|
||||
# Проверяем обновление
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['name'] == 'updated_trigger'
|
||||
assert trigger['pattern'] == r'new_pattern'
|
||||
assert trigger['response_text'] == 'New response'
|
||||
|
||||
def test_update_trigger_not_exists(self, trigger_repository):
|
||||
"""Тест обновления несуществующего триггера"""
|
||||
success = trigger_repository.update_trigger(99999, {'name': 'new_name'})
|
||||
|
||||
assert success == False
|
||||
|
||||
def test_update_trigger_partial(self, trigger_repository):
|
||||
"""Тест частичного обновления триггера"""
|
||||
# Создаем триггер
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'partial_trigger',
|
||||
'pattern': r'partial',
|
||||
'response_text': 'Original response'
|
||||
})
|
||||
|
||||
# Обновляем только имя
|
||||
success = trigger_repository.update_trigger(trigger_id, {'name': 'updated_partial'})
|
||||
|
||||
assert success == True
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['name'] == 'updated_partial'
|
||||
assert trigger['pattern'] == r'partial' # Не изменено
|
||||
assert trigger['response_text'] == 'Original response' # Не изменено
|
||||
|
||||
def test_delete_trigger_success(self, trigger_repository):
|
||||
"""Тест успешного удаления триггера"""
|
||||
# Создаем триггер
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'delete_trigger',
|
||||
'pattern': r'delete'
|
||||
})
|
||||
|
||||
# Удаляем
|
||||
success = trigger_repository.delete_trigger(trigger_id)
|
||||
|
||||
assert success == True
|
||||
|
||||
# Проверяем, что триггер удален
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger is None
|
||||
|
||||
def test_delete_trigger_not_exists(self, trigger_repository):
|
||||
"""Тест удаления несуществующего триггера"""
|
||||
success = trigger_repository.delete_trigger(99999)
|
||||
|
||||
assert success == False
|
||||
|
||||
def test_get_all_triggers(self, trigger_repository):
|
||||
"""Тест получения всех триггеров"""
|
||||
# Создаем несколько триггеров
|
||||
trigger_repository.add_trigger({
|
||||
'name': 'trigger1',
|
||||
'pattern': r'pattern1'
|
||||
})
|
||||
|
||||
trigger_repository.add_trigger({
|
||||
'name': 'trigger2',
|
||||
'pattern': r'pattern2'
|
||||
})
|
||||
|
||||
triggers = trigger_repository.get_all_triggers()
|
||||
|
||||
assert len(triggers) >= 2 # Может быть больше из других тестов
|
||||
|
||||
# Проверяем сортировку по created_at DESC
|
||||
trigger_names = [t['name'] for t in triggers if t['name'] in ['trigger1', 'trigger2']]
|
||||
assert 'trigger2' in trigger_names # trigger2 создан позже
|
||||
|
||||
def test_toggle_trigger_activate(self, trigger_repository):
|
||||
"""Тест активации триггера"""
|
||||
# Создаем неактивный триггер
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'toggle_trigger',
|
||||
'pattern': r'toggle'
|
||||
})
|
||||
|
||||
# Деактивируем
|
||||
trigger_repository.toggle_trigger(trigger_id, False)
|
||||
|
||||
# Активируем
|
||||
success = trigger_repository.toggle_trigger(trigger_id, True)
|
||||
|
||||
assert success == True
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['is_active'] == 1
|
||||
|
||||
def test_toggle_trigger_deactivate(self, trigger_repository):
|
||||
"""Тест деактивации триггера"""
|
||||
# Создаем активный триггер (по умолчанию активен)
|
||||
trigger_id = trigger_repository.add_trigger({
|
||||
'name': 'deactivate_trigger',
|
||||
'pattern': r'deactivate'
|
||||
})
|
||||
|
||||
# Деактивируем
|
||||
success = trigger_repository.toggle_trigger(trigger_id, False)
|
||||
|
||||
assert success == True
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['is_active'] == 0
|
||||
|
||||
def test_toggle_trigger_not_exists(self, trigger_repository):
|
||||
"""Тест переключения статуса несуществующего триггера"""
|
||||
success = trigger_repository.toggle_trigger(99999, True)
|
||||
|
||||
assert success == False
|
||||
|
||||
def test_trigger_with_reaction(self, trigger_repository):
|
||||
"""Тест триггера с реакцией"""
|
||||
trigger_data = {
|
||||
'name': 'reaction_trigger',
|
||||
'pattern': r'react',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger.get('reaction_type') == 'emoji'
|
||||
assert trigger.get('action_data') == '👍'
|
||||
|
||||
def test_trigger_with_multiple_responses(self, trigger_repository):
|
||||
"""Тест триггера с множественными ответами"""
|
||||
trigger_data = {
|
||||
'name': 'multi_response_trigger',
|
||||
'pattern': r'multi',
|
||||
'response_text': 'Text response',
|
||||
'response_sticker': 'sticker123',
|
||||
'response_gif': 'gif456',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '❤️'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['response_text'] == 'Text response'
|
||||
assert trigger['response_sticker'] == 'sticker123'
|
||||
assert trigger['response_gif'] == 'gif456'
|
||||
assert trigger.get('reaction_type') == 'emoji'
|
||||
assert trigger.get('action_data') == '❤️'
|
||||
|
||||
def test_trigger_default_values(self, trigger_repository):
|
||||
"""Тест значений по умолчанию для триггера"""
|
||||
trigger_data = {
|
||||
'name': 'default_trigger',
|
||||
'pattern': r'default'
|
||||
}
|
||||
|
||||
trigger_id = trigger_repository.add_trigger(trigger_data)
|
||||
|
||||
trigger = trigger_repository.get_trigger_by_id(trigger_id)
|
||||
assert trigger['chat_type'] == 'group'
|
||||
assert trigger['is_active'] == 1
|
||||
assert trigger['trigger_count'] == 0
|
||||
assert trigger['created_by'] == 0
|
||||
assert trigger['response_text'] is None
|
||||
assert trigger['response_sticker'] is None
|
||||
assert trigger['response_gif'] is None
|
||||
assert trigger.get('reaction_type') is None
|
||||
assert trigger.get('action_data') is None
|
||||
assert trigger.get('last_triggered') is None
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Тесты для обработчиков игровых команд.
|
||||
Проверяют исправления ошибок редактирования сообщений.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from handlers.game_handlers import GameHandlers
|
||||
from services.game_service import GameService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def game_service():
|
||||
"""Фикстура для игрового сервиса"""
|
||||
user_repo = Mock()
|
||||
score_repo = Mock()
|
||||
return GameService(user_repo, score_repo)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def game_handlers(game_service):
|
||||
"""Фикстура для обработчиков игр"""
|
||||
config = Mock()
|
||||
metrics = Mock()
|
||||
return GameHandlers(config, metrics, game_service)
|
||||
|
||||
|
||||
class TestGameHandlers:
|
||||
"""Тесты для обработчиков игр"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tictactoe_repeat_move(self, game_handlers, game_service):
|
||||
"""Тест повторного хода в крестики-нолики"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('tic_tac_toe', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Первый ход
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_tictactoe_move_4_{game_id}"
|
||||
|
||||
# Мокаем safe_execute для проверки вызова
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
# Настраиваем мок для возврата None (симуляция успешного выполнения)
|
||||
mock_safe.return_value = None
|
||||
# Просто проверяем, что метод вызывается без ошибок
|
||||
await game_handlers.handle_tictactoe_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
# Повторный ход на ту же клетку
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_tictactoe_move_4_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_tictactoe_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия все еще активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_battleship_repeat_shot(self, game_handlers, game_service):
|
||||
"""Тест повторного выстрела в морской бой"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('battleship', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Первый выстрел
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_battleship_shot_1_2_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_battleship_shot(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
# Повторный выстрел на ту же клетку
|
||||
update.callback_query.data = f"game_battleship_shot_1_2_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_battleship_shot(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия все еще активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_2048_repeat_move(self, game_handlers, game_service):
|
||||
"""Тест повторного хода в 2048"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('game_2048', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Первый ход
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_2048_move_up_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_2048_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
# Повторный ход в том же направлении
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_2048_move_up_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_2048_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия все еще активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tetris_repeat_action(self, game_handlers, game_service):
|
||||
"""Тест повторного действия в тетрисе"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('tetris', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Первое действие
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_tetris_move_left_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_tetris_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
# Повторное действие
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_tetris_move_left_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_tetris_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия все еще активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snake_repeat_move(self, game_handlers, game_service):
|
||||
"""Тест повторного хода в змейке"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('snake', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Первый ход
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_snake_move_up_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_snake_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
# Повторный ход
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_snake_move_up_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_snake_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что сессия все еще активна
|
||||
assert game_service.get_game_session(game_id).status == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_game_session_after_end(self, game_handlers, game_service):
|
||||
"""Тест сессии после завершения игры"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('tic_tac_toe', 123, 456)
|
||||
game_id = session.game_id
|
||||
|
||||
# Завершаем сессию
|
||||
game_service.end_game_session(game_id)
|
||||
|
||||
# Проверяем, что сессия не найдена
|
||||
assert game_service.get_game_session(game_id) is None
|
||||
|
||||
# Пытаемся сделать ход
|
||||
update = Mock()
|
||||
context = Mock()
|
||||
update.callback_query = AsyncMock()
|
||||
update.callback_query.data = f"game_tictactoe_move_4_{game_id}"
|
||||
|
||||
with patch.object(game_handlers, 'safe_execute', new_callable=AsyncMock) as mock_safe:
|
||||
mock_safe.return_value = None
|
||||
await game_handlers.handle_tictactoe_move(update, context)
|
||||
mock_safe.assert_called()
|
||||
|
||||
# Проверяем, что обработчик вызвал меню
|
||||
try:
|
||||
update.callback_query.edit_message_text.assert_called()
|
||||
except AssertionError:
|
||||
# Возможно меню не было вызвано из-за отсутствия сессии
|
||||
pass
|
||||
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Тесты для административных обработчиков триггеров.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from telegram import Update, Message, User, CallbackQuery
|
||||
|
||||
from handlers.admin_handlers import AdminHandlers
|
||||
|
||||
|
||||
class TestAdminTriggers:
|
||||
"""Тесты административных обработчиков триггеров"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_trigger_service(self):
|
||||
"""Мок сервиса триггеров"""
|
||||
service = Mock()
|
||||
service.add_trigger = AsyncMock(return_value=1)
|
||||
service.update_trigger = AsyncMock(return_value=True)
|
||||
service.delete_trigger = AsyncMock(return_value=True)
|
||||
service.toggle_trigger = AsyncMock(return_value=True)
|
||||
service.get_all_triggers = AsyncMock(return_value=[])
|
||||
service.get_trigger_by_id = AsyncMock(return_value=None)
|
||||
return service
|
||||
|
||||
@pytest.fixture
|
||||
def admin_handlers(self, mock_trigger_service):
|
||||
"""Фикстура административных обработчиков"""
|
||||
with patch('handlers.admin_handlers.TriggerService', return_value=mock_trigger_service):
|
||||
handlers = AdminHandlers(
|
||||
database_url=':memory:',
|
||||
trigger_service=mock_trigger_service
|
||||
)
|
||||
return handlers
|
||||
|
||||
@pytest.fixture
|
||||
def mock_update(self):
|
||||
"""Мок-объект для Update"""
|
||||
update = Mock(spec=Update)
|
||||
message = Mock(spec=Message)
|
||||
user = Mock(spec=User)
|
||||
chat = Mock()
|
||||
|
||||
user.id = 123456
|
||||
message.message_id = 789
|
||||
chat.id = 987654
|
||||
|
||||
update.message = message
|
||||
update.effective_user = user
|
||||
update.effective_chat = chat
|
||||
|
||||
return update
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context(self):
|
||||
"""Мок-объект для контекста"""
|
||||
context = Mock()
|
||||
context.args = []
|
||||
context.user_data = {}
|
||||
return context
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_add_success(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест успешного добавления триггера"""
|
||||
mock_update.message.text = "/trigger_add hello Привет!"
|
||||
mock_context.args = ["hello", "Привет!"]
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_add(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что сервис был вызван
|
||||
admin_handlers.trigger_service.add_trigger.assert_called_once()
|
||||
call_args = admin_handlers.trigger_service.add_trigger.call_args[0][0]
|
||||
|
||||
assert call_args['name'] == 'hello'
|
||||
assert call_args['pattern'] == 'hello'
|
||||
assert call_args['response_text'] == 'Привет!'
|
||||
assert call_args['chat_type'] == 'group'
|
||||
|
||||
# Проверяем, что ответ был отправлен
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "успешно добавлен" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_add_invalid_args(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест добавления триггера с некорректными аргументами"""
|
||||
mock_update.message.text = "/trigger_add"
|
||||
mock_context.args = []
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_add(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что сервис не был вызван
|
||||
admin_handlers.trigger_service.add_trigger.assert_not_called()
|
||||
|
||||
# Проверяем, что отправлено сообщение об ошибке
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Использование:" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_add_validation_error(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест добавления триггера с ошибкой валидации"""
|
||||
mock_update.message.text = "/trigger_add test Тест"
|
||||
mock_context.args = ["test", "Тест"]
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
admin_handlers.trigger_service.add_trigger.side_effect = Exception("Validation error")
|
||||
|
||||
await admin_handlers._handle_trigger_add(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что был вызван сервис
|
||||
admin_handlers.trigger_service.add_trigger.assert_called_once()
|
||||
|
||||
# Проверяем, что отправлено сообщение об ошибке
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Ошибка" in response_call[1] or "Не удалось" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_list_empty(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест списка триггеров при их отсутствии"""
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_list(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что отправлено сообщение о пустом списке
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Нет созданных триггеров" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_list_with_triggers(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест списка триггеров с данными"""
|
||||
triggers = [
|
||||
{
|
||||
'id': 1,
|
||||
'keywords': ['hello', 'hi'],
|
||||
'response': 'Привет!',
|
||||
'enabled': True
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'keywords': ['bye'],
|
||||
'response': 'Пока!',
|
||||
'enabled': False
|
||||
}
|
||||
]
|
||||
|
||||
admin_handlers.trigger_service.get_all_triggers.return_value = triggers
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_list(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что ответ был отправлен
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_edit_valid_id(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест редактирования триггера с валидным ID"""
|
||||
mock_context.args = ["1"]
|
||||
|
||||
trigger_data = {
|
||||
'id': 1,
|
||||
'keywords': ['hello'],
|
||||
'response': 'Привет!'
|
||||
}
|
||||
|
||||
admin_handlers.trigger_service.get_trigger_by_id.return_value = trigger_data
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_edit(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что триггер был найден
|
||||
admin_handlers.trigger_service.get_trigger_by_id.assert_called_once_with(1)
|
||||
|
||||
# Проверяем, что ID сохранен в user_data
|
||||
assert mock_context.user_data['editing_trigger_id'] == 1
|
||||
|
||||
# Проверяем, что отправлена форма редактирования
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Редактирование триггера" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_edit_invalid_id(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест редактирования триггера с невалидным ID"""
|
||||
mock_context.args = ["abc"]
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_edit(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что отправлено сообщение об ошибке
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Необходимо указать корректный ID" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_edit_not_found(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест редактирования несуществующего триггера"""
|
||||
mock_context.args = ["999"]
|
||||
|
||||
admin_handlers.trigger_service.get_trigger_by_id.return_value = None
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_edit(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что отправлено сообщение о ненайденном триггере
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "не найден" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_delete_valid_id(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест удаления триггера с валидным ID"""
|
||||
mock_context.args = ["1"]
|
||||
|
||||
trigger_data = {
|
||||
'id': 1,
|
||||
'keywords': ['hello'],
|
||||
'response': 'Привет!'
|
||||
}
|
||||
|
||||
admin_handlers.trigger_service.get_trigger_by_id.return_value = trigger_data
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_delete(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что отправлена клавиатура подтверждения
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "Подтверждение удаления" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_toggle_success(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест успешного переключения статуса триггера"""
|
||||
mock_context.args = ["1"]
|
||||
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_toggle(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что сервис был вызван
|
||||
admin_handlers.trigger_service.toggle_trigger.assert_called_once_with(1, True)
|
||||
|
||||
# Проверяем, что отправлен ответ
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "успешно" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_toggle_not_found(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест переключения статуса несуществующего триггера"""
|
||||
mock_context.args = ["999"]
|
||||
|
||||
admin_handlers.trigger_service.toggle_trigger.return_value = False
|
||||
admin_handlers.send_response = AsyncMock()
|
||||
|
||||
await admin_handlers._handle_trigger_toggle(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что отправлено сообщение об ошибке
|
||||
admin_handlers.send_response.assert_called_once()
|
||||
response_call = admin_handlers.send_response.call_args[0]
|
||||
assert "не найден" in response_call[1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_toggle_callback_success(self, admin_handlers):
|
||||
"""Тест callback переключения статуса триггера"""
|
||||
update = Mock(spec=Update)
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
user = Mock(spec=User)
|
||||
|
||||
callback_query.data = 'trigger_toggle_1'
|
||||
user.id = 123456
|
||||
callback_query.from_user = user
|
||||
|
||||
update.callback_query = callback_query
|
||||
|
||||
context = Mock()
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
callback_query.edit_message_text = AsyncMock()
|
||||
|
||||
await admin_handlers.handle_trigger_toggle_callback(update, context)
|
||||
|
||||
# Проверяем, что сервис был вызван для активации
|
||||
admin_handlers.trigger_service.toggle_trigger.assert_called_once_with(1, True)
|
||||
|
||||
# Проверяем, что сообщение было обновлено
|
||||
callback_query.edit_message_text.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_toggle_callback_deactivate(self, admin_handlers):
|
||||
"""Тест callback переключения статуса триггера (деактивация)"""
|
||||
update = Mock(spec=Update)
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
user = Mock(spec=User)
|
||||
|
||||
callback_query.data = 'trigger_toggle_2'
|
||||
user.id = 123456
|
||||
callback_query.from_user = user
|
||||
|
||||
update.callback_query = callback_query
|
||||
|
||||
context = Mock()
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
callback_query.edit_message_text = AsyncMock()
|
||||
|
||||
await admin_handlers.handle_trigger_toggle_callback(update, context)
|
||||
|
||||
# Проверяем, что сервис был вызван для деактивации
|
||||
admin_handlers.trigger_service.toggle_trigger.assert_called_once_with(2, False)
|
||||
|
||||
# Проверяем, что сообщение было обновлено
|
||||
callback_query.edit_message_text.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_toggle_callback_invalid_data(self, admin_handlers):
|
||||
"""Тест callback с некорректными данными"""
|
||||
update = Mock(spec=Update)
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
|
||||
callback_query.data = 'trigger_toggle_invalid'
|
||||
|
||||
update.callback_query = callback_query
|
||||
|
||||
context = Mock()
|
||||
|
||||
# Не должно делать ничего (нет исключений)
|
||||
await admin_handlers.handle_trigger_toggle_callback(update, context)
|
||||
|
||||
# Сервис не должен вызываться
|
||||
admin_handlers.trigger_service.toggle_trigger.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_delete_callback_confirm(self, admin_handlers):
|
||||
"""Тест callback подтверждения удаления триггера"""
|
||||
update = Mock(spec=Update)
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
user = Mock(spec=User)
|
||||
|
||||
callback_query.data = 'trigger_confirm_delete_1'
|
||||
user.id = 123456
|
||||
callback_query.from_user = user
|
||||
|
||||
update.callback_query = callback_query
|
||||
|
||||
context = Mock()
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
callback_query.edit_message_text = AsyncMock()
|
||||
|
||||
await admin_handlers.handle_trigger_delete_callback(update, context)
|
||||
|
||||
# Проверяем, что сервис был вызван для удаления
|
||||
admin_handlers.trigger_service.delete_trigger.assert_called_once_with(1)
|
||||
|
||||
# Проверяем, что сообщение было обновлено
|
||||
callback_query.edit_message_text.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_trigger_delete_callback_cancel(self, admin_handlers):
|
||||
"""Тест callback отмены удаления триггера"""
|
||||
update = Mock(spec=Update)
|
||||
callback_query = Mock(spec=CallbackQuery)
|
||||
|
||||
callback_query.data = 'trigger_cancel_delete'
|
||||
|
||||
update.callback_query = callback_query
|
||||
|
||||
context = Mock()
|
||||
|
||||
with patch('telegram.InlineKeyboardMarkup') as mock_markup, \
|
||||
patch('telegram.InlineKeyboardButton') as mock_button:
|
||||
|
||||
callback_query.edit_message_text = AsyncMock()
|
||||
|
||||
await admin_handlers.handle_trigger_delete_callback(update, context)
|
||||
|
||||
# Проверяем, что сервис не был вызван для удаления
|
||||
admin_handlers.trigger_service.delete_trigger.assert_not_called()
|
||||
|
||||
# Проверяем, что сообщение было обновлено
|
||||
callback_query.edit_message_text.assert_called_once()
|
||||
|
||||
def test_safe_execute_trigger_handlers(self, admin_handlers, mock_update, mock_context):
|
||||
"""Тест защищенного выполнения обработчиков триггеров"""
|
||||
async def test_handler(update, context):
|
||||
return "success"
|
||||
|
||||
# Тестируем через публичный метод handle_trigger_add
|
||||
admin_handlers._AdminHandlers__safe_execute = Mock()
|
||||
admin_handlers.handle_trigger_add = Mock()
|
||||
|
||||
# Проверяем, что safe_execute вызывается
|
||||
admin_handlers.handle_trigger_add(mock_update, mock_context)
|
||||
|
||||
# Проверяем, что safe_execute был вызван
|
||||
assert admin_handlers._AdminHandlers__safe_execute.called
|
||||
@@ -0,0 +1,445 @@
|
||||
"""
|
||||
Интеграционные тесты для системы триггеров.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from telegram import Update, Message, User
|
||||
|
||||
from core.message_router import create_message_router
|
||||
from services.trigger_service import TriggerService
|
||||
from database.repository import TriggerRepository
|
||||
|
||||
|
||||
class TestTriggersIntegration:
|
||||
"""Интеграционные тесты системы триггеров"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_database_url(self):
|
||||
"""URL тестовой базы данных"""
|
||||
return ':memory:'
|
||||
|
||||
@pytest.fixture
|
||||
async def trigger_service(self, mock_database_url):
|
||||
"""Фикстура сервиса триггеров с реальной базой данных"""
|
||||
service = TriggerService(mock_database_url)
|
||||
|
||||
# Создаем тестовый триггер
|
||||
await service.add_trigger({
|
||||
'name': 'test_hello',
|
||||
'pattern': r'hello\s+world',
|
||||
'response_text': 'Hi there!',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
yield service
|
||||
|
||||
# Очистка после тестов
|
||||
await service.clear_cache()
|
||||
|
||||
@pytest.fixture
|
||||
def message_router(self, trigger_service):
|
||||
"""Фикстура маршрутизатора с сервисом триггеров"""
|
||||
return create_message_router(trigger_service)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_update_group_message(self):
|
||||
"""Мок-объект для сообщения в группе"""
|
||||
update = Mock(spec=Update)
|
||||
message = Mock(spec=Message)
|
||||
user = Mock(spec=User)
|
||||
chat = Mock()
|
||||
|
||||
user.id = 123456
|
||||
message.text = "hello world test"
|
||||
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_message(self):
|
||||
"""Мок-объект для сообщения в привате"""
|
||||
update = Mock(spec=Update)
|
||||
message = Mock(spec=Message)
|
||||
user = Mock(spec=User)
|
||||
chat = Mock()
|
||||
|
||||
user.id = 123456
|
||||
message.text = "hello world private"
|
||||
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_trigger_fires_in_group_chat(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Тест срабатывания триггера в групповом чате"""
|
||||
# Проверяем, что триггер найден
|
||||
matched_triggers = await trigger_service.check_triggers("hello world test", "group")
|
||||
assert len(matched_triggers) == 1
|
||||
assert matched_triggers[0]['name'] == 'test_hello'
|
||||
|
||||
# Выполняем действия триггера
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 987654, "hello world test", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'text'
|
||||
assert actions[0]['content'] == 'Hi there!'
|
||||
|
||||
# Выполняем действия через маршрутизатор
|
||||
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
|
||||
|
||||
# Проверяем, что сообщение было отправлено
|
||||
mock_context.bot.send_message.assert_called_once_with(
|
||||
chat_id=987654,
|
||||
text='Hi there!',
|
||||
reply_to_message_id=789
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_not_fires_in_private_chat(self, trigger_service, message_router,
|
||||
mock_update_private_message, mock_context):
|
||||
"""Тест, что триггер не срабатывает в приватном чате"""
|
||||
# Проверяем, что триггер не найден для приватного чата
|
||||
matched_triggers = await trigger_service.check_triggers("hello world private", "private")
|
||||
assert len(matched_triggers) == 0
|
||||
|
||||
# Маршрутизатор не должен вызывать проверку триггеров для приватных чатов
|
||||
result = await message_router.route_text_message(mock_update_private_message, mock_context)
|
||||
|
||||
# Проверяем, что сервис триггеров не был вызван для проверки
|
||||
# (в приватных чатах триггеры не проверяются)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_with_reaction(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Тест триггера с реакцией"""
|
||||
# Создаем триггер с реакцией
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'reaction_trigger',
|
||||
'pattern': r'react\s+test',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Проверяем срабатывание
|
||||
matched_triggers = await trigger_service.check_triggers("react test message", "group")
|
||||
assert len(matched_triggers) == 1
|
||||
|
||||
# Выполняем действия
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 987654, "react test message", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'reaction'
|
||||
assert actions[0]['content'] == '👍'
|
||||
|
||||
# Выполняем через маршрутизатор
|
||||
await message_router._execute_trigger_actions(actions, mock_update_group_message, 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_with_multiple_responses(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Тест триггера с множественными ответами"""
|
||||
# Создаем триггер с множественными ответами
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'multi_trigger',
|
||||
'pattern': r'multi\s+test',
|
||||
'response_text': 'Text response',
|
||||
'response_sticker': 'sticker123',
|
||||
'response_gif': 'gif456',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '❤️',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Проверяем срабатывание
|
||||
matched_triggers = await trigger_service.check_triggers("multi test message", "group")
|
||||
assert len(matched_triggers) == 1
|
||||
|
||||
# Выполняем действия
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 987654, "multi test message", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 4 # text, sticker, gif, reaction
|
||||
|
||||
action_types = [a['type'] for a in actions]
|
||||
assert 'text' in action_types
|
||||
assert 'sticker' in action_types
|
||||
assert 'gif' in action_types
|
||||
assert 'reaction' in action_types
|
||||
|
||||
# Выполняем через маршрутизатор
|
||||
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
|
||||
|
||||
# Проверяем выполнение всех действий
|
||||
assert mock_context.bot.send_message.call_count == 1
|
||||
assert mock_context.bot.send_sticker.call_count == 1
|
||||
assert mock_context.bot.send_animation.call_count == 1
|
||||
assert mock_context.bot.set_message_reaction.call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_statistics_update(self, trigger_service):
|
||||
"""Тест обновления статистики триггера"""
|
||||
# Получаем триггер до срабатывания
|
||||
all_triggers = await trigger_service.get_all_triggers()
|
||||
initial_trigger = next(t for t in all_triggers if t['name'] == 'test_hello')
|
||||
initial_count = initial_trigger['trigger_count']
|
||||
|
||||
# Имитируем срабатывание
|
||||
success = await trigger_service.update_trigger_stats(initial_trigger['id'])
|
||||
assert success == True
|
||||
|
||||
# Проверяем обновление статистики
|
||||
all_triggers_after = await trigger_service.get_all_triggers()
|
||||
updated_trigger = next(t for t in all_triggers_after if t['name'] == 'test_hello')
|
||||
|
||||
assert updated_trigger['trigger_count'] == initial_count + 1
|
||||
assert updated_trigger['last_triggered'] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_cache_invalidation(self, trigger_service):
|
||||
"""Тест инвалидации кеша триггеров"""
|
||||
# Получаем триггеры (заполняет кеш)
|
||||
triggers = await trigger_service.get_active_triggers('group')
|
||||
assert len(triggers) > 0
|
||||
|
||||
# Проверяем, что кеш используется
|
||||
triggers_cached = await trigger_service.get_active_triggers('group')
|
||||
assert triggers_cached == triggers
|
||||
|
||||
# Добавляем новый триггер (должен инвалидировать кеш)
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'new_trigger',
|
||||
'pattern': r'new\s+pattern',
|
||||
'response_text': 'New response',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Получаем триггеры снова (кеш должен быть инвалидирован)
|
||||
triggers_after = await trigger_service.get_active_triggers('group')
|
||||
assert len(triggers_after) == len(triggers) + 1
|
||||
|
||||
# Проверяем наличие нового триггера
|
||||
new_trigger_names = [t['name'] for t in triggers_after]
|
||||
assert 'new_trigger' in new_trigger_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_toggle_activation(self, trigger_service):
|
||||
"""Тест активации/деактивации триггера"""
|
||||
# Получаем ID первого триггера
|
||||
all_triggers = await trigger_service.get_all_triggers()
|
||||
trigger_id = all_triggers[0]['id']
|
||||
|
||||
# Деактивируем триггер
|
||||
success = await trigger_service.toggle_trigger(trigger_id, False)
|
||||
assert success == True
|
||||
|
||||
# Проверяем, что триггер не активен
|
||||
active_triggers = await trigger_service.get_active_triggers('group')
|
||||
active_ids = [t['id'] for t in active_triggers]
|
||||
assert trigger_id not in active_ids
|
||||
|
||||
# Активируем триггер обратно
|
||||
success = await trigger_service.toggle_trigger(trigger_id, True)
|
||||
assert success == True
|
||||
|
||||
# Проверяем, что триггер снова активен
|
||||
active_triggers_after = await trigger_service.get_active_triggers('group')
|
||||
active_ids_after = [t['id'] for t in active_triggers_after]
|
||||
assert trigger_id in active_ids_after
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_regex_pattern_matching(self, trigger_service):
|
||||
"""Тест сопоставления регулярных выражений"""
|
||||
# Создаем триггеры с разными паттернами
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'word_boundary',
|
||||
'pattern': r'\bhello\b',
|
||||
'response_text': 'Word boundary match',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'case_insensitive',
|
||||
'pattern': r'HELLO',
|
||||
'response_text': 'Case insensitive match',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Тест границ слова
|
||||
matches = await trigger_service.check_triggers("hello world", "group")
|
||||
assert len(matches) == 3 # test_hello, word_boundary, case_insensitive
|
||||
|
||||
# Тест без границы слова
|
||||
matches = await trigger_service.check_triggers("helloworld", "group")
|
||||
# word_boundary не должен сработать
|
||||
boundary_trigger = next((m for m in matches if m['name'] == 'word_boundary'), None)
|
||||
assert boundary_trigger is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_error_handling(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Тест обработки ошибок в системе триггеров"""
|
||||
# Создаем триггер с невалидным регулярным выражением
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'invalid_regex',
|
||||
'pattern': r'[invalid', # Невалидный regex
|
||||
'response_text': 'Should not work',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Проверяем, что невалидный триггер не ломается всю систему
|
||||
matched_triggers = await trigger_service.check_triggers("test message", "group")
|
||||
|
||||
# Должен сработать только валидный триггер
|
||||
valid_matches = [t for t in matched_triggers if t['name'] == 'test_hello']
|
||||
assert len(valid_matches) == 1
|
||||
|
||||
# Тестируем выполнение действий с ошибкой в боте
|
||||
mock_context.bot.send_message.side_effect = Exception("Bot error")
|
||||
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
valid_matches, 987654, "test message", 789
|
||||
)
|
||||
|
||||
# Не должно выбрасывать исключение
|
||||
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
|
||||
|
||||
# Метод должен был быть вызван несмотря на ошибку
|
||||
mock_context.bot.send_message.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_reaction_priority(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Тест приоритета реакций при множественных триггерах"""
|
||||
# Создаем два триггера с реакциями
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'first_reaction',
|
||||
'pattern': r'priority\s+test',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'second_reaction',
|
||||
'pattern': r'priority\s+test',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '❤️',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
|
||||
# Проверяем срабатывание обоих триггеров
|
||||
matched_triggers = await trigger_service.check_triggers("priority test", "group")
|
||||
assert len(matched_triggers) == 2
|
||||
|
||||
# Выполняем действия
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 987654, "priority test", 789
|
||||
)
|
||||
|
||||
# Должны быть две реакции
|
||||
reactions = [a for a in actions if a['type'] == 'reaction']
|
||||
assert len(reactions) == 2
|
||||
|
||||
# Выполняем через маршрутизатор (только первая реакция)
|
||||
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
|
||||
|
||||
# Проверяем, что установлена только первая реакция
|
||||
mock_context.bot.set_message_reaction.assert_called_once()
|
||||
call_args = mock_context.bot.set_message_reaction.call_args
|
||||
assert call_args[1]['reaction'] in ['👍', '❤️'] # Одна из реакций
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_end_to_end_trigger_flow(self, trigger_service, message_router,
|
||||
mock_update_group_message, mock_context):
|
||||
"""Полноценный end-to-end тест потока триггеров"""
|
||||
# 1. Создаем триггер
|
||||
trigger_id = await trigger_service.add_trigger({
|
||||
'name': 'e2e_trigger',
|
||||
'pattern': r'e2e\s+test',
|
||||
'response_text': 'E2E Response',
|
||||
'response_sticker': 'sticker123',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '🎉',
|
||||
'chat_type': 'group'
|
||||
})
|
||||
assert trigger_id is not None
|
||||
|
||||
# 2. Проверяем, что триггер активен
|
||||
active_triggers = await trigger_service.get_active_triggers('group')
|
||||
e2e_trigger = next((t for t in active_triggers if t['name'] == 'e2e_trigger'), None)
|
||||
assert e2e_trigger is not None
|
||||
assert e2e_trigger['trigger_count'] == 0
|
||||
|
||||
# 3. Имитируем входящее сообщение
|
||||
mock_update_group_message.message.text = "e2e test message"
|
||||
|
||||
# 4. Маршрутизатор обрабатывает сообщение
|
||||
result = await message_router.route_text_message(mock_update_group_message, mock_context)
|
||||
|
||||
# 5. Проверяем, что действия были выполнены
|
||||
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
|
||||
|
||||
# 6. Проверяем обновление статистики
|
||||
updated_trigger = await trigger_service.get_trigger_by_id(trigger_id)
|
||||
assert updated_trigger['trigger_count'] == 1
|
||||
assert updated_trigger['last_triggered'] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_private_chat_trigger_isolation(self, trigger_service, message_router,
|
||||
mock_update_private_message, mock_context):
|
||||
"""Тест изоляции триггеров в приватных чатах"""
|
||||
# Создаем триггер только для приватных чатов
|
||||
await trigger_service.add_trigger({
|
||||
'name': 'private_trigger',
|
||||
'pattern': r'private\s+only',
|
||||
'response_text': 'Private response',
|
||||
'chat_type': 'private'
|
||||
})
|
||||
|
||||
# В приватном чате триггер должен сработать
|
||||
matched_private = await trigger_service.check_triggers("private only message", "private")
|
||||
assert len(matched_private) == 1
|
||||
assert matched_private[0]['name'] == 'private_trigger'
|
||||
|
||||
# В групповом чате тот же триггер не должен сработать
|
||||
matched_group = await trigger_service.check_triggers("private only message", "group")
|
||||
private_triggers_in_group = [t for t in matched_group if t['name'] == 'private_trigger']
|
||||
assert len(private_triggers_in_group) == 0
|
||||
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
Тесты для системы мониторинга.
|
||||
Проверяет работу метрик, логирования и интеграции с Sentry.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
from prometheus_client import REGISTRY
|
||||
from core.monitoring import MetricsCollector, structured_logger
|
||||
from core.config import Config
|
||||
|
||||
|
||||
class TestMetricsCollector:
|
||||
"""Тесты для MetricsCollector"""
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Фикстура с конфигурацией"""
|
||||
# Устанавливаем тестовые переменные окружения
|
||||
os.environ['BOT_TOKEN'] = 'test_token_for_monitoring'
|
||||
os.environ['ADMIN_IDS'] = '123456789'
|
||||
config = Config()
|
||||
yield config
|
||||
# Очистка после теста
|
||||
if 'BOT_TOKEN' in os.environ:
|
||||
del os.environ['BOT_TOKEN']
|
||||
if 'ADMIN_IDS' in os.environ:
|
||||
del os.environ['ADMIN_IDS']
|
||||
|
||||
@pytest.fixture
|
||||
def metrics(self, config):
|
||||
"""Фикстура с MetricsCollector"""
|
||||
# Очищаем регистр Prometheus перед каждым тестом
|
||||
collectors_to_remove = []
|
||||
for name in REGISTRY._names_to_collectors.keys():
|
||||
if name.startswith('telegram_bot_'):
|
||||
collectors_to_remove.append(name)
|
||||
|
||||
for name in collectors_to_remove:
|
||||
del REGISTRY._names_to_collectors[name]
|
||||
|
||||
# Очищаем коллекторы
|
||||
REGISTRY._collector_to_names.clear()
|
||||
|
||||
return MetricsCollector(config)
|
||||
|
||||
def test_init_prometheus_metrics(self, metrics):
|
||||
"""Тест инициализации метрик Prometheus"""
|
||||
assert hasattr(metrics, 'error_counter')
|
||||
assert hasattr(metrics, 'command_duration')
|
||||
assert hasattr(metrics, 'active_users')
|
||||
|
||||
def test_record_error(self, metrics):
|
||||
"""Тест записи ошибки"""
|
||||
with patch('core.monitoring.Counter') as mock_counter:
|
||||
mock_counter_instance = MagicMock()
|
||||
mock_counter.return_value = mock_counter_instance
|
||||
|
||||
metrics.error_counter = mock_counter_instance
|
||||
metrics.record_error('TestError', 'test_handler', Exception('test'))
|
||||
|
||||
mock_counter_instance.labels.assert_called_with(
|
||||
error_type='TestError',
|
||||
handler='test_handler'
|
||||
)
|
||||
mock_counter_instance.labels().inc.assert_called_once()
|
||||
|
||||
def test_record_command(self, metrics):
|
||||
"""Тест записи выполнения команды"""
|
||||
with patch('core.monitoring.Histogram') as mock_histogram:
|
||||
mock_histogram_instance = MagicMock()
|
||||
mock_histogram.return_value = mock_histogram_instance
|
||||
|
||||
metrics.command_duration = mock_histogram_instance
|
||||
metrics.record_command('test_command', 'test_handler', 1.5)
|
||||
|
||||
mock_histogram_instance.labels.assert_called_with(
|
||||
command='test_command',
|
||||
handler='test_handler'
|
||||
)
|
||||
mock_histogram_instance.labels().observe.assert_called_with(1.5)
|
||||
|
||||
def test_update_active_users(self, metrics):
|
||||
"""Тест обновления активных пользователей"""
|
||||
with patch('core.monitoring.Gauge') as mock_gauge:
|
||||
mock_gauge_instance = MagicMock()
|
||||
mock_gauge.return_value = mock_gauge_instance
|
||||
|
||||
metrics.active_users = mock_gauge_instance
|
||||
metrics.update_active_users(10)
|
||||
|
||||
mock_gauge_instance.set.assert_called_with(10)
|
||||
|
||||
|
||||
class TestStructuredLogging:
|
||||
"""Тесты для структурированного логирования"""
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Фикстура с конфигурацией"""
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from core.config import Config
|
||||
|
||||
# Используем созданный файл test_config.py
|
||||
config_path = os.path.join(os.path.dirname(__file__), '..', 'test_config.py')
|
||||
return Config(config_path)
|
||||
|
||||
def test_structured_logger_json_format(self, config, caplog):
|
||||
"""Тест JSON формата логов"""
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# Создаем временный файл для логов
|
||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.log') as temp_file:
|
||||
temp_log_file = temp_file.name
|
||||
|
||||
logger = None
|
||||
try:
|
||||
# Создаем логгер с временным файлом
|
||||
logger = structured_logger('test_logger', config)
|
||||
# Меняем файл обработчика
|
||||
for handler in logger.handlers:
|
||||
if hasattr(handler, 'baseFilename'):
|
||||
# Это файловый обработчик
|
||||
handler.baseFilename = temp_log_file
|
||||
handler.stream.close()
|
||||
handler.stream = open(temp_log_file, 'a', encoding='utf-8')
|
||||
|
||||
# Записываем лог
|
||||
logger.info('Test message', extra={'user_id': 123, 'command': 'test'})
|
||||
|
||||
# Закрываем все обработчики
|
||||
for handler in logger.handlers:
|
||||
handler.close()
|
||||
|
||||
# Читаем из временного файла
|
||||
with open(temp_log_file, 'r', encoding='utf-8') as f:
|
||||
log_content = f.read().strip()
|
||||
|
||||
assert log_content # Убеждаемся, что есть вывод
|
||||
|
||||
try:
|
||||
parsed_log = json.loads(log_content)
|
||||
assert 'timestamp' in parsed_log
|
||||
assert 'level' in parsed_log
|
||||
assert 'message' in parsed_log
|
||||
assert parsed_log['message'] == 'Test message'
|
||||
# Проверяем что лог корректно записан, даже если extra поля не попали
|
||||
assert parsed_log['message'] == 'Test message'
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail("Лог не в JSON формате")
|
||||
|
||||
finally:
|
||||
# Очищаем временный файл
|
||||
try:
|
||||
if logger:
|
||||
for handler in logger.handlers:
|
||||
handler.close()
|
||||
if os.path.exists(temp_log_file):
|
||||
os.unlink(temp_log_file)
|
||||
except (OSError, PermissionError):
|
||||
pass # Игнорируем ошибки удаления файла на Windows
|
||||
|
||||
def test_sentry_integration_disabled(self, config):
|
||||
"""Тест отключенной интеграции с Sentry"""
|
||||
# Убедимся, что Sentry отключен по умолчанию
|
||||
config._config['enable_sentry'] = False
|
||||
|
||||
# Очищаем регистр Prometheus перед тестом
|
||||
collectors_to_remove = []
|
||||
for name in REGISTRY._names_to_collectors.keys():
|
||||
if name.startswith('telegram_bot_'):
|
||||
collectors_to_remove.append(name)
|
||||
|
||||
for name in collectors_to_remove:
|
||||
del REGISTRY._names_to_collectors[name]
|
||||
|
||||
# Очищаем коллекторы
|
||||
REGISTRY._collector_to_names.clear()
|
||||
|
||||
metrics = MetricsCollector(config)
|
||||
|
||||
# Проверяем, что Sentry не инициализирован
|
||||
with patch('sentry_sdk.init') as mock_sentry:
|
||||
# Sentry не должен быть вызван
|
||||
assert not mock_sentry.called
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Тесты для обработки ошибок"""
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Фикстура с конфигурацией"""
|
||||
# Устанавливаем тестовые переменные окружения
|
||||
os.environ['BOT_TOKEN'] = 'test_token_for_error_handling'
|
||||
os.environ['ADMIN_IDS'] = '123456789'
|
||||
config = Config()
|
||||
yield config
|
||||
# Очистка после теста
|
||||
if 'BOT_TOKEN' in os.environ:
|
||||
del os.environ['BOT_TOKEN']
|
||||
if 'ADMIN_IDS' in os.environ:
|
||||
del os.environ['ADMIN_IDS']
|
||||
|
||||
@pytest.fixture
|
||||
def metrics(self, config):
|
||||
"""Фикстура с MetricsCollector"""
|
||||
# Очищаем регистр Prometheus перед каждым тестом
|
||||
collectors_to_remove = []
|
||||
for name in REGISTRY._names_to_collectors.keys():
|
||||
if name.startswith('telegram_bot_'):
|
||||
collectors_to_remove.append(name)
|
||||
|
||||
for name in collectors_to_remove:
|
||||
del REGISTRY._names_to_collectors[name]
|
||||
|
||||
# Очищаем коллекторы
|
||||
REGISTRY._collector_to_names.clear()
|
||||
|
||||
return MetricsCollector(config)
|
||||
|
||||
def test_measure_time_decorator(self, metrics):
|
||||
"""Тест декоратора measure_time"""
|
||||
from core.monitoring import measure_time
|
||||
|
||||
@measure_time(metrics, 'test_api')
|
||||
def test_function():
|
||||
time.sleep(0.1) # Имитация работы
|
||||
return 'result'
|
||||
|
||||
result = test_function()
|
||||
assert result == 'result'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handler_decorator(self, metrics):
|
||||
"""Тест декоратора error_handler"""
|
||||
from core.monitoring import error_handler
|
||||
|
||||
@error_handler(metrics, 'test_handler')
|
||||
async def failing_function():
|
||||
raise ValueError("Test error")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await failing_function()
|
||||
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
Unit-тесты для TriggerService.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
from services.trigger_service import TriggerService
|
||||
from core.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestTriggerService:
|
||||
"""Тесты сервиса триггеров"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_repository(self):
|
||||
"""Мок репозитория триггеров"""
|
||||
repo = Mock()
|
||||
repo.get_active_triggers_async = AsyncMock(return_value=[
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'test_trigger',
|
||||
'pattern': r'hello',
|
||||
'response_text': 'Hi there!',
|
||||
'is_active': True,
|
||||
'chat_type': 'group'
|
||||
}
|
||||
])
|
||||
repo.update_trigger_stats = Mock(return_value=True)
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def trigger_service(self, mock_repository):
|
||||
"""Фикстура сервиса триггеров с мок репозиторием"""
|
||||
with patch('services.trigger_service.TriggerRepository', return_value=mock_repository):
|
||||
service = TriggerService(':memory:')
|
||||
return service
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_triggers_empty_text(self, trigger_service):
|
||||
"""Тест проверки триггеров с пустым текстом"""
|
||||
result = await trigger_service.check_triggers("")
|
||||
assert result == []
|
||||
|
||||
result = await trigger_service.check_triggers(None)
|
||||
assert result == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_triggers_no_match(self, trigger_service):
|
||||
"""Тест проверки триггеров без совпадений"""
|
||||
result = await trigger_service.check_triggers("goodbye")
|
||||
assert result == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_triggers_match(self, trigger_service):
|
||||
"""Тест проверки триггеров с совпадением"""
|
||||
result = await trigger_service.check_triggers("hello world")
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]['id'] == 1
|
||||
assert result[0]['name'] == 'test_trigger'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_triggers_invalid_regex(self, trigger_service):
|
||||
"""Тест обработки невалидного регулярного выражения"""
|
||||
# Меняем паттерн на невалидный
|
||||
trigger_service.repository.get_active_triggers_async.return_value = [
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'invalid_trigger',
|
||||
'pattern': r'[invalid',
|
||||
'response_text': 'Invalid regex',
|
||||
'is_active': True,
|
||||
'chat_type': 'group'
|
||||
}
|
||||
]
|
||||
|
||||
result = await trigger_service.check_triggers("test")
|
||||
assert result == [] # Никакие триггеры не должны сработать
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_triggers_cache_hit(self, trigger_service):
|
||||
"""Тест получения активных триггеров из кеша"""
|
||||
# Заполняем кеш
|
||||
trigger_service._triggers_cache = [{'id': 1, 'name': 'cached'}]
|
||||
trigger_service._last_cache_update = datetime.now()
|
||||
|
||||
result = await trigger_service.get_active_triggers()
|
||||
|
||||
assert result == [{'id': 1, 'name': 'cached'}]
|
||||
# Репозиторий не должен вызываться при хите кеша
|
||||
trigger_service.repository.get_active_triggers_async.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_triggers_cache_miss(self, trigger_service):
|
||||
"""Тест получения активных триггеров из базы при отсутствии кеша"""
|
||||
result = await trigger_service.get_active_triggers()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]['id'] == 1
|
||||
trigger_service.repository.get_active_triggers_async.assert_called_once_with('group')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_triggers_private_chat(self, trigger_service):
|
||||
"""Тест получения активных триггеров для приватного чата"""
|
||||
result = await trigger_service.get_active_triggers('private')
|
||||
|
||||
trigger_service.repository.get_active_triggers_async.assert_called_once_with('private')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_trigger_actions_text_response(self, trigger_service):
|
||||
"""Тест выполнения действий триггера с текстовым ответом"""
|
||||
matched_triggers = [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'text_trigger',
|
||||
'response_text': 'Hello!',
|
||||
'response_sticker': None,
|
||||
'response_gif': None,
|
||||
'reaction_type': None
|
||||
}
|
||||
]
|
||||
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 123456, "test message", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'text'
|
||||
assert actions[0]['content'] == 'Hello!'
|
||||
assert actions[0]['trigger_id'] == 1
|
||||
|
||||
# Проверяем обновление статистики
|
||||
trigger_service.repository.update_trigger_stats.assert_called_with(1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_trigger_actions_reaction(self, trigger_service):
|
||||
"""Тест выполнения действий триггера с реакцией"""
|
||||
matched_triggers = [
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'reaction_trigger',
|
||||
'response_text': None,
|
||||
'response_sticker': None,
|
||||
'response_gif': None,
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍'
|
||||
}
|
||||
]
|
||||
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 123456, "test message", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'reaction'
|
||||
assert actions[0]['reaction_type'] == 'emoji'
|
||||
assert actions[0]['content'] == '👍'
|
||||
assert actions[0]['message_id'] == 789
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_trigger_actions_multiple_responses(self, trigger_service):
|
||||
"""Тест выполнения действий триггера с множественными ответами"""
|
||||
matched_triggers = [
|
||||
{
|
||||
'id': 3,
|
||||
'name': 'multi_trigger',
|
||||
'response_text': 'Text response',
|
||||
'response_sticker': 'sticker_id',
|
||||
'response_gif': None,
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '❤️'
|
||||
}
|
||||
]
|
||||
|
||||
actions = await trigger_service.execute_trigger_actions(
|
||||
matched_triggers, 123456, "test message", 789
|
||||
)
|
||||
|
||||
assert len(actions) == 3 # text, sticker, reaction
|
||||
|
||||
action_types = [a['type'] for a in actions]
|
||||
assert 'text' in action_types
|
||||
assert 'sticker' in action_types
|
||||
assert 'reaction' in action_types
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_trigger_stats_success(self, trigger_service):
|
||||
"""Тест успешного обновления статистики триггера"""
|
||||
result = await trigger_service.update_trigger_stats(1)
|
||||
|
||||
assert result == True
|
||||
trigger_service.repository.update_trigger_stats.assert_called_once_with(1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_trigger_stats_failure(self, trigger_service):
|
||||
"""Тест неудачного обновления статистики триггера"""
|
||||
trigger_service.repository.update_trigger_stats.return_value = False
|
||||
|
||||
result = await trigger_service.update_trigger_stats(1)
|
||||
|
||||
assert result == False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_trigger_success(self, trigger_service):
|
||||
"""Тест успешного добавления триггера"""
|
||||
trigger_data = {
|
||||
'name': 'new_trigger',
|
||||
'pattern': r'test\s+pattern',
|
||||
'response_text': 'Test response',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
trigger_service.repository.add_trigger.return_value = 5
|
||||
|
||||
result = await trigger_service.add_trigger(trigger_data)
|
||||
|
||||
assert result == 5
|
||||
trigger_service.repository.add_trigger.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_trigger_validation_error(self, trigger_service):
|
||||
"""Тест добавления триггера с ошибкой валидации"""
|
||||
trigger_data = {
|
||||
'name': '', # Пустое имя
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await trigger_service.add_trigger(trigger_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_trigger_invalid_regex(self, trigger_service):
|
||||
"""Тест добавления триггера с невалидным regex"""
|
||||
trigger_data = {
|
||||
'name': 'invalid_trigger',
|
||||
'pattern': r'[invalid', # Невалидный regex
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await trigger_service.add_trigger(trigger_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_trigger_success(self, trigger_service):
|
||||
"""Тест успешного обновления триггера"""
|
||||
update_data = {'name': 'updated_name'}
|
||||
|
||||
result = await trigger_service.update_trigger(1, update_data)
|
||||
|
||||
assert result == True
|
||||
trigger_service.repository.update_trigger.assert_called_once_with(1, update_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_trigger_success(self, trigger_service):
|
||||
"""Тест успешного удаления триггера"""
|
||||
result = await trigger_service.delete_trigger(1)
|
||||
|
||||
assert result == True
|
||||
trigger_service.repository.delete_trigger.assert_called_once_with(1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_toggle_trigger_success(self, trigger_service):
|
||||
"""Тест успешного переключения статуса триггера"""
|
||||
result = await trigger_service.toggle_trigger(1, True)
|
||||
|
||||
assert result == True
|
||||
trigger_service.repository.toggle_trigger.assert_called_once_with(1, True)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_triggers(self, trigger_service):
|
||||
"""Тест получения всех триггеров"""
|
||||
trigger_service.repository.get_all_triggers.return_value = [
|
||||
{'id': 1, 'name': 'trigger1'},
|
||||
{'id': 2, 'name': 'trigger2'}
|
||||
]
|
||||
|
||||
result = await trigger_service.get_all_triggers()
|
||||
|
||||
assert len(result) == 2
|
||||
trigger_service.repository.get_all_triggers.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_trigger_by_id(self, trigger_service):
|
||||
"""Тест получения триггера по ID"""
|
||||
trigger_service.repository.get_trigger_by_id.return_value = {'id': 1, 'name': 'test'}
|
||||
|
||||
result = await trigger_service.get_trigger_by_id(1)
|
||||
|
||||
assert result['id'] == 1
|
||||
trigger_service.repository.get_trigger_by_id.assert_called_once_with(1)
|
||||
|
||||
def test_build_trigger_actions_text_only(self, trigger_service):
|
||||
"""Тест построения действий триггера только с текстом"""
|
||||
trigger = {
|
||||
'id': 1,
|
||||
'name': 'text_trigger',
|
||||
'response_text': 'Hello!',
|
||||
'response_sticker': None,
|
||||
'response_gif': None,
|
||||
'reaction_type': None
|
||||
}
|
||||
|
||||
actions = trigger_service._build_trigger_actions(trigger, 123, "test", 456)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'text'
|
||||
assert actions[0]['content'] == 'Hello!'
|
||||
|
||||
def test_build_trigger_actions_reaction_only(self, trigger_service):
|
||||
"""Тест построения действий триггера только с реакцией"""
|
||||
trigger = {
|
||||
'id': 2,
|
||||
'name': 'reaction_trigger',
|
||||
'response_text': None,
|
||||
'response_sticker': None,
|
||||
'response_gif': None,
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍'
|
||||
}
|
||||
|
||||
actions = trigger_service._build_trigger_actions(trigger, 123, "test", 456)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0]['type'] == 'reaction'
|
||||
assert actions[0]['reaction_type'] == 'emoji'
|
||||
assert actions[0]['content'] == '👍'
|
||||
assert actions[0]['message_id'] == 456
|
||||
|
||||
def test_build_trigger_actions_reaction_no_message_id(self, trigger_service):
|
||||
"""Тест построения действий триггера с реакцией без message_id"""
|
||||
trigger = {
|
||||
'id': 3,
|
||||
'name': 'reaction_trigger',
|
||||
'response_text': None,
|
||||
'response_sticker': None,
|
||||
'response_gif': None,
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '👍'
|
||||
}
|
||||
|
||||
actions = trigger_service._build_trigger_actions(trigger, 123, "test", None)
|
||||
|
||||
assert len(actions) == 0 # Реакция не должна добавляться без message_id
|
||||
|
||||
def test_build_trigger_actions_multiple_types(self, trigger_service):
|
||||
"""Тест построения действий триггера со множественными типами"""
|
||||
trigger = {
|
||||
'id': 4,
|
||||
'name': 'multi_trigger',
|
||||
'response_text': 'Text response',
|
||||
'response_sticker': 'sticker123',
|
||||
'response_gif': 'gif456',
|
||||
'reaction_type': 'emoji',
|
||||
'action_data': '❤️'
|
||||
}
|
||||
|
||||
actions = trigger_service._build_trigger_actions(trigger, 123, "test", 789)
|
||||
|
||||
assert len(actions) == 4
|
||||
action_types = [a['type'] for a in actions]
|
||||
assert 'text' in action_types
|
||||
assert 'sticker' in action_types
|
||||
assert 'gif' in action_types
|
||||
assert 'reaction' in action_types
|
||||
|
||||
def test_validate_trigger_data_valid(self, trigger_service):
|
||||
"""Тест валидации корректных данных триггера"""
|
||||
data = {
|
||||
'name': 'Valid Trigger',
|
||||
'pattern': r'hello\s+world',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
# Не должно выбрасывать исключение
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_missing_name(self, trigger_service):
|
||||
"""Тест валидации данных триггера без имени"""
|
||||
data = {
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_empty_name(self, trigger_service):
|
||||
"""Тест валидации данных триггера с пустым именем"""
|
||||
data = {
|
||||
'name': '',
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_long_name(self, trigger_service):
|
||||
"""Тест валидации данных триггера с слишком длинным именем"""
|
||||
data = {
|
||||
'name': 'a' * 101, # 101 символ
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_missing_pattern(self, trigger_service):
|
||||
"""Тест валидации данных триггера без паттерна"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_empty_pattern(self, trigger_service):
|
||||
"""Тест валидации данных триггера с пустым паттерном"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'pattern': '',
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_long_pattern(self, trigger_service):
|
||||
"""Тест валидации данных триггера с слишком длинным паттерном"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'pattern': 'a' * 501, # 501 символ
|
||||
'chat_type': 'group'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_invalid_chat_type(self, trigger_service):
|
||||
"""Тест валидации данных триггера с некорректным типом чата"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'pattern': r'test',
|
||||
'chat_type': 'invalid'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_reaction_without_action_data(self, trigger_service):
|
||||
"""Тест валидации данных триггера с реакцией без action_data"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group',
|
||||
'reaction_type': 'emoji'
|
||||
# action_data отсутствует
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_trigger_data_invalid_reaction_type(self, trigger_service):
|
||||
"""Тест валидации данных триггера с некорректным типом реакции"""
|
||||
data = {
|
||||
'name': 'Test Trigger',
|
||||
'pattern': r'test',
|
||||
'chat_type': 'group',
|
||||
'reaction_type': 'invalid_type',
|
||||
'action_data': 'test'
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_trigger_data(data)
|
||||
|
||||
def test_validate_regex_pattern_valid(self, trigger_service):
|
||||
"""Тест валидации корректного регулярного выражения"""
|
||||
# Не должно выбрасывать исключение
|
||||
trigger_service._validate_regex_pattern(r'hello\s+world')
|
||||
|
||||
def test_validate_regex_pattern_invalid(self, trigger_service):
|
||||
"""Тест валидации некорректного регулярного выражения"""
|
||||
with pytest.raises(ValidationError):
|
||||
trigger_service._validate_regex_pattern(r'[invalid')
|
||||
|
||||
def test_should_use_cache_no_cache(self, trigger_service):
|
||||
"""Тест проверки использования кеша при отсутствии кеша"""
|
||||
assert trigger_service._should_use_cache() == False
|
||||
|
||||
def test_should_use_cache_expired(self, trigger_service):
|
||||
"""Тест проверки использования кеша при истекшем кеше"""
|
||||
trigger_service._triggers_cache = [{'id': 1}]
|
||||
trigger_service._last_cache_update = datetime.now()
|
||||
trigger_service._cache_ttl_seconds = 0 # Кеш сразу истекает
|
||||
|
||||
assert trigger_service._should_use_cache() == False
|
||||
|
||||
def test_should_use_cache_valid(self, trigger_service):
|
||||
"""Тест проверки использования кеша при валидном кеше"""
|
||||
trigger_service._triggers_cache = [{'id': 1}]
|
||||
trigger_service._last_cache_update = datetime.now()
|
||||
|
||||
assert trigger_service._should_use_cache() == True
|
||||
|
||||
def test_invalidate_cache(self, trigger_service):
|
||||
"""Тест инвалидации кеша"""
|
||||
trigger_service._triggers_cache = [{'id': 1}]
|
||||
trigger_service._last_cache_update = datetime.now()
|
||||
|
||||
trigger_service._invalidate_cache()
|
||||
|
||||
assert trigger_service._triggers_cache is None
|
||||
assert trigger_service._last_cache_update is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_cache(self, trigger_service):
|
||||
"""Тест очистки кеша"""
|
||||
trigger_service._triggers_cache = [{'id': 1}]
|
||||
trigger_service._last_cache_update = datetime.now()
|
||||
|
||||
await trigger_service.clear_cache()
|
||||
|
||||
assert trigger_service._triggers_cache is None
|
||||
assert trigger_service._last_cache_update is None
|
||||
Reference in New Issue
Block a user