"""
Тесты для обработчиков пользовательских команд.
Проверяет корректность обработки команд пользователями.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pytest
from datetime import datetime
from unittest.mock import Mock, AsyncMock, patch
from telegram import Update, User, Message, Chat
from telegram.ext import ContextTypes
from handlers.user_handlers import UserHandlers
from services.user_service import UserService, UserProfile
from core.exceptions import ValidationError, PermissionError
class TestUserHandlers:
"""Тесты обработчиков пользовательских команд"""
@pytest.fixture
def mock_user_service(self):
"""Мок сервиса пользователей"""
service = Mock(spec=UserService)
service.get_or_create_user = AsyncMock()
service.update_user_activity = AsyncMock()
service.get_top_users = AsyncMock()
service.get_rank_progress = Mock()
from core.permissions import UserRole
service.get_user_role_enum = Mock()
service.get_user_role_enum.return_value = UserRole.USER
# Настройка возвращаемых значений по умолчанию
service.get_top_users.return_value = []
service.get_rank_progress.return_value = {
'current_rank': 'Ученик',
'next_rank': 'Активист',
'percentage': 75.0,
'progress': 75,
'needed': 100
}
return service
@pytest.fixture
def user_handlers(self, test_config, mock_user_service):
"""Экземпляр обработчика пользовательских команд"""
from unittest.mock import Mock
mock_metrics = Mock()
return UserHandlers(test_config, mock_metrics, mock_user_service)
@pytest.fixture
def mock_update(self, mock_user, mock_chat, mock_message):
"""Мок объекта обновления"""
update = Mock(spec=Update)
update.effective_user = mock_user
update.effective_chat = mock_chat
update.message = mock_message
return update
@pytest.fixture
def mock_context(self):
"""Мок контекста"""
context = Mock(spec=ContextTypes.DEFAULT_TYPE)
context.args = []
context.user_data = {}
return context
@pytest.mark.asyncio
async def test_handle_start_success(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест успешной обработки команды /start"""
# Мокируем профиль пользователя
profile = UserProfile(
user_id=123456789,
username="test_user",
first_name="Test",
last_name="User",
reputation=100,
rank="Активист"
)
mock_user_service.get_or_create_user.return_value = profile
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Выполняем обработку команды
await user_handlers.handle_start(mock_update, mock_context)
# Проверяем, что пользователь был создан/получен
mock_user_service.get_or_create_user.assert_called_once_with(
123456789, "test_user", "Test", "User"
)
# Проверяем, что ответ был отправлен
mock_update.message.reply_text.assert_called_once()
reply_text = mock_update.message.reply_text.call_args[0][0]
# Проверяем содержание ответа
assert "Привет, Test!" in reply_text
assert "Добро пожаловать в наш бот!" in reply_text
@pytest.mark.asyncio
async def test_handle_rank_success(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест успешной обработки команды /rank"""
# Мокируем профиль пользователя
profile = UserProfile(
user_id=123456789,
username="test_user",
first_name="Test",
last_name="User",
reputation=150,
rank="Активист",
message_count=45,
warnings=2
)
mock_user_service.get_or_create_user.return_value = profile
# Мокируем прогресс ранга
rank_progress = {
'current_rank': 'Активист',
'next_rank': 'Знаток',
'percentage': 75.0
}
mock_user_service.get_rank_progress.return_value = rank_progress
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Выполняем обработку команды
await user_handlers.handle_rank(mock_update, mock_context)
# Проверяем вызовы сервиса
mock_user_service.get_or_create_user.assert_called_once_with(
123456789, "test_user", "Test", "User"
)
mock_user_service.get_rank_progress.assert_called_once_with(150)
# Проверяем ответ
mock_update.message.reply_text.assert_called_once()
reply_text = mock_update.message.reply_text.call_args[0][0]
assert "Ваш ранг:" in reply_text
assert "Очки: 150" in reply_text
assert "Ранг: Активист" in reply_text
assert "Сообщений: 45" in reply_text
@pytest.mark.asyncio
async def test_handle_leaderboard_success(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест успешной обработки команды /leaderboard"""
# Мокируем топ пользователей
top_users = [
(111, "user1", "User One", 100),
(222, "user2", "User Two", 90),
(333, None, "User Three", 80)
]
mock_user_service.get_top_users.return_value = top_users
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Выполняем обработку команды
await user_handlers.handle_leaderboard(mock_update, mock_context)
# Проверяем вызов сервиса
mock_user_service.get_top_users.assert_called_once_with(10)
# Проверяем ответ
mock_update.message.reply_text.assert_called_once()
reply_text = mock_update.message.reply_text.call_args[0][0]
assert "🏆 Таблица лидеров:" in reply_text
assert "🥇 user1 - 100 очков" in reply_text
assert "🥈 user2 - 90 очков" in reply_text
assert "🥉 User Three - 80 очков" in reply_text
@pytest.mark.asyncio
async def test_handle_leaderboard_empty(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест обработки команды /leaderboard с пустым результатом"""
mock_user_service.get_top_users.return_value = []
mock_update.message.reply_text = AsyncMock()
await user_handlers.handle_leaderboard(mock_update, mock_context)
reply_text = mock_update.message.reply_text.call_args[0][0]
assert "📭 Таблица лидеров пуста" in reply_text
@pytest.mark.asyncio
async def test_handle_info_own_profile(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест обработки команды /info для своего профиля"""
# Мокируем профиль пользователя
profile = UserProfile(
user_id=123456789,
username="test_user",
first_name="Test",
last_name="User",
reputation=150,
rank="Активист",
message_count=45,
warnings=1,
joined_date=datetime.now()
)
mock_user_service.get_or_create_user.return_value = profile
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Выполняем обработку команды
await user_handlers.handle_info(mock_update, mock_context)
# Проверяем вызов сервиса
mock_user_service.get_or_create_user.assert_called_once_with(
123456789, "test_user", "Test", "User"
)
# Проверяем ответ
reply_text = mock_update.message.reply_text.call_args[0][0]
assert "👤 Информация о пользователе:" in reply_text
assert "🆔 ID: 123456789" in reply_text
assert "👤 Имя: Test" in reply_text
assert "📱 Username: @test_user" in reply_text
@pytest.mark.asyncio
async def test_handle_info_other_user_admin(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест обработки команды /info для чужого профиля администратором"""
# Добавляем аргумент с ID пользователя
mock_context.args = ["987654321"]
# Мокируем профиль другого пользователя
other_profile = UserProfile(
user_id=987654321,
username="other_user",
first_name="Other",
last_name="User",
reputation=200,
rank="Эксперт"
)
mock_user_service.get_or_create_user.return_value = other_profile
# Мокируем ответ бота
mock_update.message.reply_text = AsyncMock()
# Выполняем обработку команды
await user_handlers.handle_info(mock_update, mock_context)
# Проверяем вызов сервиса с правильным ID
mock_user_service.get_or_create_user.assert_called_once_with(
987654321, None, None, None # Должен быть вызван с ID из аргументов, остальные None
)
# Проверяем ответ
reply_text = mock_update.message.reply_text.call_args[0][0]
assert "👤 Информация о пользователе:" in reply_text
assert "🆔 ID: 987654321" in reply_text
assert "👤 Имя: Other" in reply_text
assert "📱 Username: @other_user" in reply_text
@pytest.mark.asyncio
async def test_handle_text_message_with_plus(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест обработки текстового сообщения с плюсом"""
# Устанавливаем текст сообщения с плюсом
mock_update.message.text = "Отличная работа! +"
# Мокируем обновление активности
mock_user_service.update_user_activity.return_value = {'activity_updated': True}
# Мокируем реакцию
mock_update.message.set_reaction = AsyncMock()
# Выполняем обработку
await user_handlers.handle_text_message(mock_update, mock_context)
# Проверяем, что реакция была установлена
mock_update.message.set_reaction.assert_called_once_with("🤝")
# Проверяем обновление активности
mock_user_service.update_user_activity.assert_called_once_with(
123456789, -1001234567890
)
@pytest.mark.asyncio
async def test_handle_text_message_without_plus(self, user_handlers, mock_update, mock_context, mock_user_service):
"""Тест обработки текстового сообщения без плюса"""
mock_update.message.text = "Простое сообщение без плюса"
mock_user_service.update_user_activity.return_value = {'activity_updated': True}
mock_update.message.set_reaction = AsyncMock()
await user_handlers.handle_text_message(mock_update, mock_context)
# Реакция не должна быть установлена
mock_update.message.set_reaction.assert_not_called()
# Активность должна быть обновлена
mock_user_service.update_user_activity.assert_called_once()
@pytest.mark.asyncio
async def test_safe_execute_with_exception(self, user_handlers, mock_update, mock_context):
"""Тест безопасного выполнения с исключением"""
# Создаем функцию, которая вызывает исключение
async def failing_function(update, context):
raise ValidationError("Тестовая ошибка валидации")
# Выполняем безопасное выполнение
await user_handlers.safe_execute(
mock_update, mock_context, "test_action", failing_function
)
# Проверяем, что сообщение об ошибке было отправлено
mock_update.message.reply_text.assert_called_once()
error_text = mock_update.message.reply_text.call_args[0][0]
assert "⚠️ Тестовая ошибка валидации" in error_text
@pytest.mark.asyncio
async def test_is_admin_true(self, user_handlers, mock_update, test_config):
"""Тест проверки прав администратора (администратор)"""
# Проверяем по списку администраторов
test_config.bot_config.admin_ids = [123456789]
# Мокируем permission_manager для возврата ADMIN роли
from unittest.mock import patch
from core.permissions import UserRole
with patch('core.permissions.permission_manager.get_effective_role', return_value=UserRole.ADMIN):
is_admin = await user_handlers.is_admin(mock_update, 123456789)
assert is_admin is True
@pytest.mark.asyncio
async def test_is_admin_false(self, user_handlers, mock_update, test_config):
"""Тест проверки прав администратора (не администратор)"""
test_config.bot_config.admin_ids = [999999999] # Другой администратор
# Мокируем permission_manager для возврата USER роли
from unittest.mock import patch
from core.permissions import UserRole
with patch('core.permissions.permission_manager.get_effective_role', return_value=UserRole.USER):
is_admin = await user_handlers.is_admin(mock_update, 123456789)
assert is_admin is False
def test_extract_user_id_from_args_valid(self, user_handlers):
"""Тест извлечения ID пользователя из корректных аргументов"""
user_id = user_handlers.extract_user_id_from_args(["123456789"])
assert user_id == 123456789
def test_extract_user_id_from_args_invalid(self, user_handlers):
"""Тест извлечения ID пользователя из некорректных аргументов"""
user_id = user_handlers.extract_user_id_from_args(["not_a_number"])
assert user_id is None
user_id = user_handlers.extract_user_id_from_args([])
assert user_id is None
def test_extract_amount_from_args(self, user_handlers):
"""Тест извлечения суммы из аргументов"""
amount = user_handlers.extract_amount_from_args(["100.5"])
assert amount == 100.5
amount = user_handlers.extract_amount_from_args(["0"])
assert amount is None # Нулевая сумма недействительна
amount = user_handlers.extract_amount_from_args([])
assert amount is None
def test_validate_user_id(self, user_handlers):
"""Тест валидации ID пользователя"""
# Корректный ID
assert user_handlers.validate_user_id("123456789") == 123456789
# Некорректный ID
with pytest.raises(ValidationError):
user_handlers.validate_user_id("not_a_number")
with pytest.raises(ValidationError):
user_handlers.validate_user_id("0")
def test_format_user_mention(self, user_handlers):
"""Тест форматирования упоминания пользователя"""
mention = user_handlers.format_user_mention(123456789, "Test User")
assert mention == "[Test User](tg://user?id=123456789)"