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,136 @@
|
||||
"""
|
||||
Тесты для сервиса управления играми.
|
||||
Проверяет бизнес-логику игровых механик.
|
||||
|
||||
Включает:
|
||||
- Создание и управление игровыми сессиями
|
||||
- Игру в камень-ножницы-бумага с поддержкой русского ввода
|
||||
- Обработку ошибок и валидацию
|
||||
- Очистку старых сессий
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from services.game_service import GameService, GameSession
|
||||
from core.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestGameService:
|
||||
"""Тесты сервиса игр"""
|
||||
|
||||
@pytest.fixture
|
||||
def user_repo(self):
|
||||
"""Мок репозитория пользователей"""
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def score_repo(self):
|
||||
"""Мок репозитория очков"""
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def game_service(self, user_repo, score_repo):
|
||||
"""Экземпляр сервиса игр"""
|
||||
return GameService(user_repo, score_repo)
|
||||
|
||||
def test_create_game_session_rock_paper_scissors(self, game_service):
|
||||
"""Тест создания сессии для камень-ножницы-бумага"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
|
||||
assert isinstance(session, GameSession)
|
||||
assert session.game_type == 'rock_paper_scissors'
|
||||
assert session.player_id == 123456789
|
||||
assert session.chat_id == -1001234567890
|
||||
assert session.status == "active"
|
||||
assert session.game_id.startswith('roc_')
|
||||
assert session.data['player_choice'] is None
|
||||
assert session.data['bot_choice'] is None
|
||||
assert session.data['result'] is None
|
||||
|
||||
def test_create_game_session_invalid_type(self, game_service):
|
||||
"""Тест создания сессии с неверным типом игры"""
|
||||
with pytest.raises(ValidationError, match="Неизвестный тип игры"):
|
||||
game_service.create_game_session('invalid_game', 123456789, -1001234567890)
|
||||
|
||||
def test_get_game_session(self, game_service):
|
||||
"""Тест получения игровой сессии"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
retrieved = game_service.get_game_session(session.game_id)
|
||||
|
||||
assert retrieved == session
|
||||
|
||||
def test_get_game_session_not_found(self, game_service):
|
||||
"""Тест получения несуществующей сессии"""
|
||||
retrieved = game_service.get_game_session('nonexistent_id')
|
||||
|
||||
assert retrieved is None
|
||||
|
||||
def test_end_game_session(self, game_service):
|
||||
"""Тест завершения игровой сессии"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
result = game_service.end_game_session(session.game_id)
|
||||
|
||||
assert result is True
|
||||
assert session.status == "completed"
|
||||
assert game_service.get_game_session(session.game_id) is None
|
||||
|
||||
def test_end_game_session_not_found(self, game_service):
|
||||
"""Тест завершения несуществующей сессии"""
|
||||
result = game_service.end_game_session('nonexistent_id')
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_play_rock_paper_scissors_english(self, game_service):
|
||||
"""Тест игры в RPS с английским выбором"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
|
||||
result = game_service.play_rock_paper_scissors(session.game_id, 'rock')
|
||||
|
||||
assert result['result'] in ['win', 'draw', 'lose']
|
||||
assert result['player_choice'] == 'rock'
|
||||
assert result['bot_choice'] in ['rock', 'paper', 'scissors']
|
||||
assert 'points' in result
|
||||
|
||||
def test_play_rock_paper_scissors_russian(self, game_service):
|
||||
"""Тест игры в RPS с русским выбором.
|
||||
|
||||
Проверяет поддержку русского ввода: 'камень' -> 'rock'
|
||||
"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
|
||||
result = game_service.play_rock_paper_scissors(session.game_id, 'камень')
|
||||
|
||||
assert result['result'] in ['win', 'draw', 'lose']
|
||||
assert result['player_choice'] == 'rock'
|
||||
assert result['bot_choice'] in ['rock', 'paper', 'scissors']
|
||||
assert 'points' in result
|
||||
|
||||
def test_play_rock_paper_scissors_invalid_choice(self, game_service):
|
||||
"""Тест игры в RPS с неверным выбором"""
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
|
||||
with pytest.raises(ValidationError, match="Неверный выбор"):
|
||||
game_service.play_rock_paper_scissors(session.game_id, 'invalid')
|
||||
|
||||
def test_play_rock_paper_scissors_game_not_found(self, game_service):
|
||||
"""Тест игры в RPS с несуществующей сессией"""
|
||||
with pytest.raises(ValidationError, match="Игра не найдена или неверный тип игры"):
|
||||
game_service.play_rock_paper_scissors('nonexistent_id', 'rock')
|
||||
|
||||
def test_play_rock_paper_scissors_wrong_game_type(self, game_service):
|
||||
"""Тест игры в RPS с неверным типом игры"""
|
||||
session = game_service.create_game_session('quiz', 123456789, -1001234567890)
|
||||
|
||||
with pytest.raises(ValidationError, match="Игра не найдена или неверный тип игры"):
|
||||
game_service.play_rock_paper_scissors(session.game_id, 'rock')
|
||||
|
||||
def test_cleanup_old_sessions(self, game_service):
|
||||
"""Тест очистки старых сессий"""
|
||||
# Создаем сессию
|
||||
session = game_service.create_game_session('rock_paper_scissors', 123456789, -1001234567890)
|
||||
|
||||
# Очищаем старые сессии (должна очиститься, если сессия старая)
|
||||
game_service.cleanup_old_sessions(-1) # -1 минута - все старые
|
||||
|
||||
# Проверяем, что сессия удалена
|
||||
assert game_service.get_game_session(session.game_id) is None
|
||||
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
Юнит и интеграционные тесты для NotificationService.
|
||||
Тестирование улучшенных функций: retry, rate limiting, статистика, обработка ошибок.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
from unittest.mock import Mock, AsyncMock, patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.notification_service import NotificationService
|
||||
from telegram.error import TelegramError, NetworkError, TimedOut, RetryAfter
|
||||
|
||||
|
||||
class TestNotificationService:
|
||||
"""Тесты для NotificationService"""
|
||||
|
||||
@pytest.fixture
|
||||
def notification_service(self):
|
||||
"""Фикстура сервиса уведомлений для тестов"""
|
||||
service = NotificationService("test_token", [123456789])
|
||||
# Мокаем бота для тестов
|
||||
mock_bot = AsyncMock()
|
||||
service.bot = mock_bot
|
||||
return service
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_notification_send(self, notification_service):
|
||||
"""Тест успешной отправки уведомления"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
# Мокаем бота
|
||||
with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send:
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
# Проверяем вызов
|
||||
mock_send.assert_called_once_with(
|
||||
chat_id=user_id,
|
||||
text=message,
|
||||
parse_mode=None,
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
# Проверяем статистику
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 1
|
||||
assert stats['failed_sends'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_network_error_retry(self, notification_service):
|
||||
"""Тест повторных попыток при сетевой ошибке"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
# Мокаем бота для симуляции сетевой ошибки, затем успешной отправки
|
||||
call_count = 0
|
||||
async def mock_send(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise NetworkError("Connection failed")
|
||||
# Вторая попытка успешна
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=mock_send):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
# Проверяем, что было 2 вызова (первая неудачная, вторая успешная)
|
||||
assert call_count == 2
|
||||
|
||||
# Проверяем статистику
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 1
|
||||
assert stats['network_errors'] == 1
|
||||
assert stats['failed_sends'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_error_retry(self, notification_service):
|
||||
"""Тест повторных попыток при таймауте"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
call_count = 0
|
||||
async def mock_send(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count <= 2:
|
||||
raise TimedOut("Request timeout")
|
||||
# Третья попытка успешна
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=mock_send):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
assert call_count == 3
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 1
|
||||
assert stats['timeout_errors'] == 2 # Две неудачные попытки
|
||||
assert stats['failed_sends'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_handling(self, notification_service):
|
||||
"""Тест обработки rate limiting от Telegram API"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
call_count = 0
|
||||
async def mock_send(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise RetryAfter(5) # Ждать 5 секунд
|
||||
# Вторая попытка успешна
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=mock_send):
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Проверяем, что прошло время задержки (минимум 5 секунд)
|
||||
assert end_time - start_time >= 5
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 1
|
||||
assert stats['rate_limit_errors'] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_retries_exceeded(self, notification_service):
|
||||
"""Тест превышения максимального количества попыток"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
# Всегда выбрасывать сетевую ошибку
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=NetworkError("Persistent network error")):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
# Проверяем статистику
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['network_errors'] == 3 # max_retries = 3
|
||||
assert stats['failed_sends'] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exponential_backoff(self, notification_service):
|
||||
"""Тест экспоненциальной задержки между попытками"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
delays = []
|
||||
call_count = 0
|
||||
|
||||
async def mock_send(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 3: # Первые две попытки неудачные
|
||||
raise NetworkError("Network error")
|
||||
# Третья успешна
|
||||
|
||||
# Фильтруем только задержки retry (не rate limiting)
|
||||
async def mock_sleep(delay):
|
||||
if delay >= 1.0: # Только задержки retry
|
||||
delays.append(delay)
|
||||
# Не используем await asyncio.sleep чтобы избежать рекурсии
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=mock_send), \
|
||||
patch('asyncio.sleep', side_effect=mock_sleep):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
# Проверяем задержки: 1s, 2s (экспоненциально)
|
||||
assert len(delays) == 2
|
||||
assert delays[0] == 1.0 # base_retry_delay
|
||||
assert delays[1] == 2.0 # base_retry_delay * 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limiting_between_requests(self, notification_service):
|
||||
"""Тест rate limiting между последовательными запросами"""
|
||||
user_id1 = 987654321
|
||||
user_id2 = 111222333
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send:
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Отправляем два уведомления подряд
|
||||
await notification_service.send_custom_notification(user_id1, message)
|
||||
await notification_service.send_custom_notification(user_id2, message)
|
||||
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Проверяем, что прошло минимум 0.1 секунды между запросами
|
||||
assert end_time - start_time >= 0.1
|
||||
|
||||
# Проверяем, что оба сообщения отправлены
|
||||
assert mock_send.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_error_handling(self, notification_service):
|
||||
"""Тест обработки общих ошибок Telegram"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=TelegramError("Invalid chat_id")):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['other_errors'] == 1
|
||||
assert stats['failed_sends'] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unexpected_error_handling(self, notification_service):
|
||||
"""Тест обработки неожиданных ошибок"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=Exception("Unexpected error")):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['other_errors'] == 1
|
||||
assert stats['failed_sends'] == 1
|
||||
|
||||
def test_stats_calculation(self, notification_service):
|
||||
"""Тест расчета статистики"""
|
||||
# Имитируем успешную отправку
|
||||
notification_service.stats['successful_sends'] = 10
|
||||
notification_service.stats['failed_sends'] = 2
|
||||
notification_service.stats['network_errors'] = 1
|
||||
notification_service.stats['timeout_errors'] = 1
|
||||
notification_service.stats['rate_limit_errors'] = 1
|
||||
notification_service.stats['other_errors'] = 1
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
|
||||
assert stats['total_attempts'] == 16 # 10 + 2 + 1 + 1 + 1 + 1
|
||||
assert stats['success_rate'] == 62.5 # 10/16 * 100
|
||||
|
||||
def test_stats_reset(self, notification_service):
|
||||
"""Тест сброса статистики"""
|
||||
# Заполняем статистику
|
||||
notification_service.stats['successful_sends'] = 5
|
||||
notification_service.stats['network_errors'] = 3
|
||||
|
||||
# Сбрасываем
|
||||
notification_service.reset_stats()
|
||||
|
||||
# Проверяем сброс
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['network_errors'] == 0
|
||||
assert stats['total_attempts'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_long_message_handling(self, notification_service):
|
||||
"""Тест отправки очень длинных сообщений"""
|
||||
user_id = 987654321
|
||||
# Создаем очень длинное сообщение (более 4096 символов)
|
||||
long_message = "A" * 5000
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send:
|
||||
await notification_service.send_custom_notification(user_id, long_message)
|
||||
|
||||
# Сообщение должно быть отправлено несмотря на длину
|
||||
mock_send.assert_called_once()
|
||||
call_args = mock_send.call_args
|
||||
assert call_args[1]['chat_id'] == user_id
|
||||
assert len(call_args[1]['text']) == 5000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_notifications(self, notification_service):
|
||||
"""Тест одновременной отправки множества уведомлений"""
|
||||
import asyncio
|
||||
|
||||
user_ids = [100000000 + i for i in range(10)]
|
||||
message = "Массовое уведомление"
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send:
|
||||
# Создаем задачи для одновременной отправки
|
||||
tasks = [
|
||||
notification_service.send_custom_notification(user_id, message)
|
||||
for user_id in user_ids
|
||||
]
|
||||
|
||||
# Выполняем все задачи параллельно
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Проверяем, что все сообщения отправлены
|
||||
assert mock_send.call_count == 10
|
||||
|
||||
# Проверяем статистику
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_unavailable_simulation(self, notification_service):
|
||||
"""Тест симуляции недоступности Telegram API"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
# Имитируем недоступность API (все попытки неудачные)
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=NetworkError("API unavailable")):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['network_errors'] == 3 # max_retries
|
||||
assert stats['failed_sends'] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mixed_error_types(self, notification_service):
|
||||
"""Тест смеси различных типов ошибок"""
|
||||
user_id = 987654321
|
||||
message = "Тестовое сообщение"
|
||||
|
||||
call_count = 0
|
||||
async def mock_send(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise TimedOut("Timeout")
|
||||
elif call_count == 2:
|
||||
raise NetworkError("Network error")
|
||||
elif call_count == 3:
|
||||
raise TelegramError("Other error")
|
||||
# Не должно дойти до четвертой попытки
|
||||
|
||||
with patch.object(notification_service.bot, 'send_message', side_effect=mock_send):
|
||||
await notification_service.send_custom_notification(user_id, message)
|
||||
|
||||
stats = notification_service.get_notification_stats()
|
||||
assert stats['successful_sends'] == 0
|
||||
assert stats['timeout_errors'] == 1
|
||||
assert stats['network_errors'] == 1
|
||||
assert stats['other_errors'] == 1
|
||||
assert stats['failed_sends'] == 1
|
||||
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Тесты для сервиса управления пользователями.
|
||||
Проверяет бизнес-логику работы с пользователями.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from datetime import datetime
|
||||
from services.user_service import UserService, UserProfile
|
||||
from core.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestUserService:
|
||||
"""Тесты сервиса пользователей"""
|
||||
|
||||
@pytest.fixture
|
||||
def user_repo(self):
|
||||
"""Мок репозитория пользователей"""
|
||||
repo = Mock()
|
||||
repo.get_by_id_async = AsyncMock()
|
||||
repo.get_by_id = AsyncMock()
|
||||
repo.create_user = AsyncMock()
|
||||
repo.update_user = AsyncMock()
|
||||
repo.update_activity = AsyncMock()
|
||||
repo.get_top_users_async = AsyncMock()
|
||||
repo.search_users = AsyncMock()
|
||||
repo.add_warning = AsyncMock()
|
||||
repo.get_warnings_count = AsyncMock()
|
||||
repo.update_rank = AsyncMock()
|
||||
repo.get_days_active = AsyncMock()
|
||||
repo.add_donation = AsyncMock()
|
||||
repo.begin_transaction = AsyncMock()
|
||||
repo.commit_transaction = AsyncMock()
|
||||
repo.rollback_transaction = AsyncMock()
|
||||
repo._execute_query_async = AsyncMock()
|
||||
repo._fetch_one_async = AsyncMock()
|
||||
|
||||
# Настройка возвращаемых значений по умолчанию
|
||||
repo.get_by_id_async.return_value = None
|
||||
repo.get_by_id.return_value = None
|
||||
repo.create_user.return_value = None
|
||||
repo.update_user.return_value = None
|
||||
repo.update_activity.return_value = None
|
||||
repo.get_top_users_async.return_value = []
|
||||
repo.search_users.return_value = []
|
||||
repo.add_warning.return_value = True
|
||||
repo.get_warnings_count.return_value = 0
|
||||
repo.update_rank.return_value = None
|
||||
repo.get_days_active.return_value = 0
|
||||
repo.add_donation.return_value = True
|
||||
repo._execute_query_async.return_value = True
|
||||
repo._fetch_one_async.return_value = {'total': 0.0}
|
||||
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def score_repo(self):
|
||||
"""Мок репозитория очков"""
|
||||
repo = Mock()
|
||||
repo.update_score = AsyncMock()
|
||||
repo.get_total_score = AsyncMock()
|
||||
repo.get_message_count = AsyncMock()
|
||||
repo.begin_transaction = AsyncMock()
|
||||
repo.commit_transaction = AsyncMock()
|
||||
repo.rollback_transaction = AsyncMock()
|
||||
|
||||
# Настройка возвращаемых значений по умолчанию
|
||||
repo.update_score.return_value = True
|
||||
repo.get_total_score.return_value = 0
|
||||
repo.get_message_count.return_value = 0
|
||||
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def user_service(self, user_repo, score_repo):
|
||||
"""Экземпляр сервиса пользователей"""
|
||||
return UserService(user_repo, score_repo)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_user_existing(self, user_service, user_repo, score_repo):
|
||||
"""Тест получения существующего пользователя"""
|
||||
# Мокируем данные пользователя
|
||||
existing_user_data = {
|
||||
'id': 123456789,
|
||||
'telegram_id': 123456789,
|
||||
'username': 'test_user',
|
||||
'first_name': 'Test',
|
||||
'last_name': 'User',
|
||||
'reputation': 100,
|
||||
'rank': 'Активист'
|
||||
}
|
||||
user_repo.get_by_id_async = AsyncMock(return_value=existing_user_data)
|
||||
user_repo.get_by_id = AsyncMock(return_value=existing_user_data)
|
||||
user_repo.create_user.return_value = None
|
||||
|
||||
# Выполняем тест
|
||||
profile = await user_service.get_or_create_user(123456789, 'test_user', 'Test', 'User')
|
||||
|
||||
# Проверяем результат
|
||||
assert isinstance(profile, UserProfile)
|
||||
assert profile.user_id == 123456789
|
||||
assert profile.username == 'test_user'
|
||||
assert profile.first_name == 'Test'
|
||||
assert profile.reputation == 100
|
||||
assert profile.rank == 'Активист'
|
||||
|
||||
# Проверяем, что репозиторий был вызван корректно (должен быть вызван 2 раза: один раз в начале, второй в _update_user_data)
|
||||
assert user_repo.get_by_id_async.call_count == 1
|
||||
assert user_repo.get_by_id.call_count == 1
|
||||
user_repo.get_by_id_async.assert_any_call(123456789)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_user_new(self, user_service, user_repo, score_repo):
|
||||
"""Тест создания нового пользователя"""
|
||||
# Мокируем отсутствие существующего пользователя
|
||||
# Мокируем создание пользователя
|
||||
new_user_data = {
|
||||
'id': 123456789,
|
||||
'telegram_id': 123456789,
|
||||
'username': 'new_user',
|
||||
'first_name': 'New',
|
||||
'last_name': 'User'
|
||||
}
|
||||
|
||||
# Настраиваем мок так, чтобы первый вызов возвращал None, а второй - данные пользователя
|
||||
user_repo.get_by_id_async = AsyncMock(side_effect=[None, new_user_data])
|
||||
user_repo.get_by_id = AsyncMock(return_value=new_user_data)
|
||||
user_repo.create_user = AsyncMock(return_value=new_user_data)
|
||||
user_repo._execute_query_async = AsyncMock(return_value=True)
|
||||
|
||||
# Выполняем тест
|
||||
profile = await user_service.get_or_create_user(123456789, 'new_user', 'New', 'User')
|
||||
|
||||
# Проверяем результат
|
||||
assert isinstance(profile, UserProfile)
|
||||
assert profile.user_id == 123456789
|
||||
assert profile.username == 'new_user'
|
||||
|
||||
# Проверяем вызовы методов (get_by_id_async должен быть вызван 1 раз, get_by_id 1 раз)
|
||||
assert user_repo.get_by_id_async.call_count == 1
|
||||
assert user_repo.get_by_id.call_count == 1
|
||||
user_repo.get_by_id_async.assert_any_call(123456789)
|
||||
user_repo._execute_query_async.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_activity(self, user_service, user_repo, score_repo):
|
||||
"""Тест обновления активности пользователя"""
|
||||
# Мокируем обновление активности
|
||||
activity_data = {'updated': True}
|
||||
user_repo.update_activity.return_value = activity_data
|
||||
score_repo.update_score.return_value = True
|
||||
|
||||
# Выполняем тест
|
||||
result = await user_service.update_user_activity(123456789, -1001234567890)
|
||||
|
||||
# Проверяем результат
|
||||
assert result['activity_updated'] is True
|
||||
assert result['rank_promoted'] is False # По умолчанию
|
||||
|
||||
# Проверяем вызовы
|
||||
user_repo.update_activity.assert_called_once_with(123456789, -1001234567890)
|
||||
score_repo.update_score.assert_called_once_with(123456789, 1)
|
||||
|
||||
def test_calculate_rank(self, user_service):
|
||||
"""Тест расчета ранга пользователя"""
|
||||
# Тестируем различные пороги рангов
|
||||
assert user_service.calculate_rank(50) == "Новичок"
|
||||
assert user_service.calculate_rank(150) == "Ученик"
|
||||
assert user_service.calculate_rank(750) == "Активист"
|
||||
assert user_service.calculate_rank(1500) == "Знаток"
|
||||
assert user_service.calculate_rank(3000) == "Эксперт"
|
||||
|
||||
def test_get_rank_progress(self, user_service):
|
||||
"""Тест получения прогресса до следующего ранга"""
|
||||
progress = user_service.get_rank_progress(150)
|
||||
|
||||
assert progress['current_rank'] == "Ученик"
|
||||
assert progress['next_rank'] == "Активист"
|
||||
assert progress['current_score'] == 150
|
||||
assert 'percentage' in progress
|
||||
assert isinstance(progress['percentage'], float)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_warning(self, user_service, user_repo):
|
||||
"""Тест добавления предупреждения"""
|
||||
user_repo.add_warning.return_value = True
|
||||
|
||||
result = await user_service.add_warning(123456789, "Нарушение правил", 987654321)
|
||||
|
||||
assert result is True
|
||||
user_repo.add_warning.assert_called_once_with(123456789, "Нарушение правил", 987654321)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_top_users(self, user_service, user_repo):
|
||||
"""Тест получения топ пользователей"""
|
||||
top_users = [
|
||||
(111, "user1", "User One", 100),
|
||||
(222, "user2", "User Two", 90)
|
||||
]
|
||||
user_repo.get_top_users_async.return_value = top_users
|
||||
|
||||
result = await user_service.get_top_users(5)
|
||||
|
||||
assert result == top_users
|
||||
user_repo.get_top_users_async.assert_called_once_with(5)
|
||||
|
||||
def test_calculate_rank_edge_cases(self, user_service):
|
||||
"""Тест расчета ранга в краевых случаях"""
|
||||
# Минимальное значение
|
||||
assert user_service.calculate_rank(0) == "Новичок"
|
||||
assert user_service.calculate_rank(-100) == "Новичок"
|
||||
|
||||
# Максимальное значение
|
||||
assert user_service.calculate_rank(999999999) == "Император"
|
||||
|
||||
def test_get_rank_progress_max_rank(self, user_service):
|
||||
"""Тест прогресса для максимального ранга"""
|
||||
progress = user_service.get_rank_progress(300000) # Император
|
||||
|
||||
assert progress['current_rank'] == "Император"
|
||||
assert progress['next_rank'] == "Император"
|
||||
assert progress['percentage'] == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_donation_success(self, user_service, user_repo, score_repo):
|
||||
"""Тест успешного добавления доната"""
|
||||
# Мокируем пользователя
|
||||
user_profile = UserProfile(
|
||||
user_id=123456789,
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
username="test_user"
|
||||
)
|
||||
|
||||
# Мокируем методы
|
||||
user_service.get_or_create_user = AsyncMock(return_value=user_profile)
|
||||
user_service.check_and_unlock_achievements = AsyncMock(return_value=[])
|
||||
user_repo.add_donation = AsyncMock(return_value=True)
|
||||
score_repo.update_score.return_value = True
|
||||
|
||||
# Выполняем тест
|
||||
result = await user_service.add_donation(123456789, 500.0)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is True
|
||||
|
||||
# Проверяем вызовы транзакций
|
||||
user_repo.begin_transaction.assert_called_once()
|
||||
score_repo.begin_transaction.assert_called_once()
|
||||
user_repo.commit_transaction.assert_called_once()
|
||||
score_repo.commit_transaction.assert_called_once()
|
||||
user_repo.rollback_transaction.assert_not_called()
|
||||
score_repo.rollback_transaction.assert_not_called()
|
||||
|
||||
# Проверяем бизнес-логику
|
||||
user_repo.add_donation.assert_called_once_with(123456789, 500.0, 2025) # текущий год
|
||||
score_repo.update_score.assert_called_once_with(123456789, 5) # 500 // 100 = 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_donation_failure_rollback(self, user_service, user_repo, score_repo):
|
||||
"""Тест отката транзакции при ошибке"""
|
||||
# Мокируем пользователя
|
||||
user_profile = UserProfile(
|
||||
user_id=123456789,
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
username="test_user"
|
||||
)
|
||||
|
||||
# Мокируем методы - донат не добавляется
|
||||
user_service.get_or_create_user = AsyncMock(return_value=user_profile)
|
||||
user_repo.add_donation.return_value = False
|
||||
|
||||
# Выполняем тест
|
||||
result = await user_service.add_donation(123456789, 500.0)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is False
|
||||
|
||||
# Проверяем вызовы транзакций
|
||||
user_repo.begin_transaction.assert_called_once()
|
||||
score_repo.begin_transaction.assert_called_once()
|
||||
user_repo.rollback_transaction.assert_called_once()
|
||||
score_repo.rollback_transaction.assert_called_once()
|
||||
user_repo.commit_transaction.assert_not_called()
|
||||
score_repo.commit_transaction.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_donation_exception_rollback(self, user_service, user_repo, score_repo):
|
||||
"""Тест отката транзакции при исключении"""
|
||||
# Мокируем пользователя
|
||||
user_profile = UserProfile(
|
||||
user_id=123456789,
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
username="test_user"
|
||||
)
|
||||
|
||||
# Мокируем методы - исключение при обновлении очков
|
||||
user_service.get_or_create_user = AsyncMock(return_value=user_profile)
|
||||
user_repo.add_donation.return_value = True
|
||||
score_repo.update_score.side_effect = Exception("Database error")
|
||||
|
||||
# Выполняем тест
|
||||
result = await user_service.add_donation(123456789, 500.0)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is False
|
||||
|
||||
# Проверяем вызовы транзакций
|
||||
user_repo.begin_transaction.assert_called_once()
|
||||
score_repo.begin_transaction.assert_called_once()
|
||||
user_repo.rollback_transaction.assert_called_once()
|
||||
score_repo.rollback_transaction.assert_called_once()
|
||||
user_repo.commit_transaction.assert_not_called()
|
||||
score_repo.commit_transaction.assert_not_called()
|
||||
|
||||
progress = user_service.get_rank_progress(300000) # Император
|
||||
|
||||
assert progress['current_rank'] == "Император"
|
||||
assert progress['next_rank'] == "Император"
|
||||
assert progress['percentage'] == 100
|
||||
|
||||
|
||||
class TestUserProfile:
|
||||
"""Тесты профиля пользователя"""
|
||||
|
||||
def test_user_profile_creation(self):
|
||||
"""Тест создания профиля пользователя"""
|
||||
profile = UserProfile(
|
||||
user_id=123456789,
|
||||
username="test_user",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
reputation=150,
|
||||
rank="Активист"
|
||||
)
|
||||
|
||||
assert profile.user_id == 123456789
|
||||
assert profile.username == "test_user"
|
||||
assert profile.first_name == "Test"
|
||||
assert profile.reputation == 150
|
||||
assert profile.rank == "Активист"
|
||||
assert profile.achievements == []
|
||||
|
||||
def test_user_profile_defaults(self):
|
||||
"""Тест значений по умолчанию профиля пользователя"""
|
||||
profile = UserProfile(
|
||||
user_id=123456789,
|
||||
first_name="Test",
|
||||
username=None,
|
||||
last_name=None
|
||||
)
|
||||
|
||||
assert profile.username is None
|
||||
assert profile.last_name is None
|
||||
assert profile.reputation == 0
|
||||
assert profile.rank == "Новичок"
|
||||
assert profile.message_count == 0
|
||||
assert profile.warnings == 0
|
||||
assert profile.achievements == []
|
||||
assert isinstance(profile.joined_date, datetime)
|
||||
assert isinstance(profile.last_activity, datetime)
|
||||
Reference in New Issue
Block a user