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

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+111
View File
@@ -0,0 +1,111 @@
"""
Конфигурация pytest для тестов обработчиков.
"""
import os
import sys
import tempfile
import pytest
from datetime import datetime
from unittest.mock import Mock, AsyncMock
from telegram import Update, User, Message, Chat, CallbackQuery
from telegram.ext import ContextTypes
# Добавляем корневую директорию в путь
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.config import Config
from services.user_service import UserService
@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 = [123456789, 987654321]
OPENWEATHER_API_KEY = "test_weather_key"
NEWS_API_KEY = "test_news_key"
OPENAI_API_KEY = "test_openai_key"
""")
config_path = f.name
try:
config = Config(config_path)
yield config
finally:
os.unlink(config_path)
@pytest.fixture
def mock_user_service():
"""Мок сервиса пользователей"""
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()
return service
@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
+377
View File
@@ -0,0 +1,377 @@
"""
Тесты для обработчиков пользовательских команд.
Проверяет корректность обработки команд пользователями.
"""
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 "🏆 <b>Таблица лидеров:</b>" in reply_text
assert "🥇 <b>user1</b> - 100 очков" in reply_text
assert "🥈 <b>user2</b> - 90 очков" in reply_text
assert "🥉 <b>User Three</b> - 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 "👤 <b>Информация о пользователе:</b>" 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 "👤 <b>Информация о пользователе:</b>" 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)"