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

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
@@ -0,0 +1,573 @@
"""
Комплексные интеграционные тесты обработки ошибок между слоями архитектуры.
Тестирование: handlers -> services -> repositories -> database
Проверяет корректную обработку и распространение ошибок через все уровни.
"""
import pytest
import tempfile
import os
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from datetime import datetime
import sqlite3
# Добавляем корневую директорию в путь
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 DatabaseError, ValidationError, PermissionError, BotException
from services.user_service import UserService
from services.game_service import GameService
from services.moderation_service import ModerationService
from services.donation_service import DonationService
from handlers.user_handlers import UserHandlers
from handlers.game_handlers import GameHandlers
from handlers.moderation_handlers import ModerationHandlers
from database import UserRepository, ScoreRepository, PaymentRepository
class TestErrorHandlingIntegration:
"""Комплексные интеграционные тесты обработки ошибок"""
@pytest.fixture
def temp_config(self):
"""Временный файл конфигурации для интеграционных тестов"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write("""
BOT_TOKEN = "123456789:integration_test_token"
ADMIN_IDS = [123456789, 987654321]
MODERATOR_IDS = [555666777]
OPENWEATHER_API_KEY = "test_key"
NEWS_API_KEY = "test_key"
OPENAI_API_KEY = "test_key"
STRIPE_SECRET_KEY = "sk_test_123456789"
YOOKASSA_SHOP_ID = "123456"
YOOKASSA_SECRET_KEY = "test_secret_key"
""")
config_path = f.name
yield config_path
# Очистка
if os.path.exists(config_path):
os.unlink(config_path)
@pytest.fixture
def temp_db(self):
"""Временная база данных для тестов с инициализацией таблиц"""
from database.models import DatabaseSchema
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Инициализируем базу данных
repo = UserRepository(db_path)
try:
# Создаем все таблицы
for table_sql in DatabaseSchema.get_create_tables_sql():
repo._execute_query(table_sql)
# Создаем стандартные достижения
repo.initialize_achievements()
finally:
repo.close()
yield db_path
# Очистка
if os.path.exists(db_path):
os.unlink(db_path)
@pytest.fixture
def mock_update(self):
"""Мокированный объект Update для команд"""
update = Mock()
# Мокаем пользователя
user = Mock()
user.id = 123456789
user.username = "test_user"
user.first_name = "Test"
user.last_name = "User"
update.effective_user = user
# Мокаем сообщение
message = Mock()
message.message_id = 1
message.reply_text = AsyncMock()
update.message = message
# Мокаем чат
chat = Mock()
chat.id = -1001234567890
update.effective_chat = chat
return update
@pytest.fixture
def mock_context(self):
"""Мокированный контекст бота"""
context = Mock()
context.args = []
# Мокаем приложение
app = Mock()
app._date_time = datetime.now()
context._application = app
# Мокаем бота
bot = AsyncMock()
context.bot = bot
context.user_data = {}
return context
@pytest.mark.asyncio
async def test_database_connection_error_propagation(self, temp_config, temp_db, mock_update, mock_context):
"""Тест распространения ошибки подключения к базе данных через все слои"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Имитируем ошибку подключения к БД
original_get_connection = user_repo._get_connection
user_repo._get_connection = Mock(side_effect=sqlite3.OperationalError("Database connection failed"))
# Шаг 2: Пытаемся выполнить команду, которая требует БД
await user_handlers.handle_start(mock_update, mock_context)
# Шаг 3: Проверяем, что ошибка была обработана
mock_update.message.reply_text.assert_called_once()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "Произошла неожиданная ошибка" in error_message
# Шаг 4: Восстанавливаем подключение
user_repo._get_connection = original_get_connection
# Шаг 5: Повторяем команду (теперь должна работать)
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_start(mock_update, mock_context)
success_message = mock_update.message.reply_text.call_args[0][0]
assert "Произошла неожиданная ошибка" not in success_message
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_validation_error_handling_chain(self, temp_config, temp_db, mock_update, mock_context):
"""Тест цепочки обработки ошибок валидации"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Имитируем ошибку валидации в сервисе
original_create_user = user_repo.create_user
user_repo.create_user = Mock(side_effect=ValidationError("Invalid user data", "username"))
# Шаг 2: Пытаемся создать пользователя через команду
await user_handlers.handle_start(mock_update, mock_context)
# Шаг 3: Проверяем обработку ошибки валидации
mock_update.message.reply_text.assert_called_once()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "Ошибка валидации" in error_message or "Произошла неожиданная ошибка" in error_message
# Шаг 4: Восстанавливаем функцию
user_repo.create_user = original_create_user
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_service_layer_error_recovery(self, temp_config, temp_db, mock_update, mock_context):
"""Тест восстановления после ошибок на уровне сервисов"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Создаем пользователя успешно
await user_handlers.handle_start(mock_update, mock_context)
# Шаг 2: Имитируем ошибку в user_service
original_get_or_create = user_service.get_or_create_user
user_service.get_or_create_user = AsyncMock(side_effect=Exception("Service temporarily unavailable"))
# Шаг 3: Пытаемся выполнить команду ранга
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_rank(mock_update, mock_context)
# Шаг 4: Проверяем обработку ошибки сервиса
mock_update.message.reply_text.assert_called_once()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "Произошла неожиданная ошибка" in error_message
# Шаг 5: Восстанавливаем сервис
user_service.get_or_create_user = original_get_or_create
# Шаг 6: Повторяем команду
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_rank(mock_update, mock_context)
success_message = mock_update.message.reply_text.call_args[0][0]
assert "Произошла неожиданная ошибка" not in success_message
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_game_service_error_handling(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок в игровом сервисе"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
game_service = GameService(user_repo, score_repo)
game_handlers = GameHandlers(config, config, game_service, config)
try:
# Шаг 1: Создаем пользователя
await user_service.get_or_create_user(
mock_update.effective_user.id,
mock_update.effective_user.username,
mock_update.effective_user.first_name,
mock_update.effective_user.last_name
)
# Шаг 2: Имитируем ошибку в игровом сервисе
original_play_rps = game_service.play_rock_paper_scissors
game_service.play_rock_paper_scissors = Mock(side_effect=Exception("Game logic error"))
# Шаг 3: Пытаемся сыграть в игру
await game_handlers._handle_play_game(mock_update, mock_context)
# Шаг 4: Проверяем обработку ошибки
mock_update.message.reply_text.assert_called()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "ошибка" in error_message.lower() or "неожиданная" in error_message.lower()
# Шаг 5: Восстанавливаем игровую логику
game_service.play_rock_paper_scissors = original_play_rps
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_moderation_service_error_handling(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок в сервисе модерации"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
moderation_service = ModerationService(user_repo, score_repo)
moderation_handlers = ModerationHandlers(config, config, user_service, moderation_service)
try:
# Шаг 1: Создаем пользователей
await user_service.get_or_create_user(
mock_update.effective_user.id,
mock_update.effective_user.username,
mock_update.effective_user.first_name,
mock_update.effective_user.last_name
)
target_user_id = 999999999
await user_service.get_or_create_user(target_user_id, "target", "Target", "User")
# Шаг 2: Имитируем ошибку в сервисе модерации
original_warn = moderation_service.warn_user
moderation_service.warn_user = AsyncMock(side_effect=PermissionError("Insufficient permissions", "moderator"))
# Шаг 3: Пытаемся выполнить модерацию
mock_context.args = [str(target_user_id), "Test warning"]
await moderation_handlers._handle_warn(mock_update, mock_context, mock_context.args)
# Шаг 4: Проверяем обработку ошибки прав доступа
mock_update.message.reply_text.assert_called_once()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "ошибка" in error_message.lower() or "неожиданная" in error_message.lower()
# Шаг 5: Восстанавливаем функцию
moderation_service.warn_user = original_warn
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_payment_service_error_handling(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок в платежном сервисе"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
payment_repo = PaymentRepository(temp_db)
user_service = UserService(user_repo, score_repo)
donation_service = DonationService(payment_repo, config.api_keys)
try:
# Шаг 1: Создаем пользователя
await user_service.get_or_create_user(
mock_update.effective_user.id,
mock_update.effective_user.username,
mock_update.effective_user.first_name,
mock_update.effective_user.last_name
)
# Шаг 2: Имитируем ошибку платежного сервиса
original_add_donation = user_service.add_donation
user_service.add_donation = AsyncMock(side_effect=Exception("Payment service unavailable"))
# Шаг 3: Пытаемся сделать донат
success = await user_service.add_donation(mock_update.effective_user.id, 500.0)
assert not success
# Шаг 4: Проверяем, что ошибка обработана на уровне сервиса
# (add_donation должен вернуть False при ошибке)
# Шаг 5: Восстанавливаем функцию
user_service.add_donation = original_add_donation
# Шаг 6: Повторяем успешный донат
success_retry = await user_service.add_donation(mock_update.effective_user.id, 500.0)
assert success_retry
finally:
user_repo.close()
score_repo.close()
payment_repo.close()
@pytest.mark.asyncio
async def test_transaction_rollback_error_handling(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок с откатом транзакций"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
try:
# Шаг 1: Имитируем ошибку в середине транзакции доната
original_update_score = score_repo.update_score
score_repo.update_score = Mock(side_effect=Exception("Transaction failed during score update"))
# Шаг 2: Пытаемся сделать донат (должен откатить транзакцию)
success = await user_service.add_donation(mock_update.effective_user.id, 500.0)
assert not success
# Шаг 3: Проверяем, что данные не изменились
user_data = user_repo.get_by_id(mock_update.effective_user.id)
if user_data:
# Если пользователь существует, очки не должны измениться
score = score_repo.get_total_score(mock_update.effective_user.id)
assert score == 0 # Должно остаться без изменений
# Шаг 4: Восстанавливаем функцию
score_repo.update_score = original_update_score
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_concurrent_error_handling(self, temp_config, temp_db, mock_update, mock_context):
"""Тест обработки ошибок при одновременных операциях"""
import asyncio
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
try:
# Шаг 1: Создаем пользователя
await user_service.get_or_create_user(
mock_update.effective_user.id,
mock_update.effective_user.username,
mock_update.effective_user.first_name,
mock_update.effective_user.last_name
)
# Шаг 2: Имитируем конкурентные ошибки
async def failing_operation():
await asyncio.sleep(0.01) # Небольшая задержка
raise DatabaseError("Concurrent access error")
# Шаг 3: Запускаем несколько операций concurrently
tasks = [failing_operation() for _ in range(3)]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Шаг 4: Проверяем, что все ошибки обработаны
for result in results:
assert isinstance(result, DatabaseError)
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_error_logging_integration(self, temp_config, temp_db, mock_update, mock_context):
"""Тест интеграции логирования ошибок"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Имитируем ошибку с логированием
with patch('handlers.base_handler.logger') as mock_logger:
# Имитируем ошибку в сервисе
original_get_or_create = user_service.get_or_create_user
user_service.get_or_create_user = AsyncMock(side_effect=Exception("Test error for logging"))
# Шаг 2: Выполняем команду
await user_handlers.handle_start(mock_update, mock_context)
# Шаг 3: Проверяем, что ошибка залогирована
mock_logger.error.assert_called()
# Шаг 4: Восстанавливаем функцию
user_service.get_or_create_user = original_get_or_create
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_error_recovery_mechanisms(self, temp_config, temp_db, mock_update, mock_context):
"""Тест механизмов восстановления после ошибок"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Создаем сценарий с последовательными ошибками и восстановлением
# Первая ошибка - сервис недоступен
original_service = user_service.get_or_create_user
user_service.get_or_create_user = AsyncMock(side_effect=Exception("Service down"))
await user_handlers.handle_start(mock_update, mock_context)
assert "ошибка" in mock_update.message.reply_text.call_args[0][0].lower()
# Восстановление сервиса
user_service.get_or_create_user = original_service
# Повторная попытка - должна работать
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_start(mock_update, mock_context)
success_message = mock_update.message.reply_text.call_args[0][0]
assert "ошибка" not in success_message.lower()
# Проверяем, что пользователь создан несмотря на первоначальную ошибку
profile = await user_service.get_or_create_user(
mock_update.effective_user.id,
mock_update.effective_user.username,
mock_update.effective_user.first_name,
mock_update.effective_user.last_name
)
assert profile.user_id == mock_update.effective_user.id
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_cascading_error_propagation(self, temp_config, temp_db, mock_update, mock_context):
"""Тест каскадного распространения ошибок через слои"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Имитируем цепочку ошибок
# Ошибка в репозитории -> сервис -> хендлер
# Ошибка в репозитории
original_repo_method = user_repo.get_by_id
user_repo.get_by_id = Mock(side_effect=sqlite3.IntegrityError("Foreign key constraint failed"))
# Ошибка в сервисе (вызывает репозиторий)
try:
await user_handlers.handle_rank(mock_update, mock_context)
except Exception:
pass # Ожидаемая ошибка
# Шаг 2: Проверяем, что ошибка обработана на уровне хендлера
mock_update.message.reply_text.assert_called()
error_message = mock_update.message.reply_text.call_args[0][0]
assert "ошибка" in error_message.lower() or "неожиданная" in error_message.lower()
# Шаг 3: Восстанавливаем и тестируем успешный случай
user_repo.get_by_id = original_repo_method
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_rank(mock_update, mock_context)
success_message = mock_update.message.reply_text.call_args[0][0]
assert "ошибка" not in success_message.lower()
finally:
user_repo.close()
score_repo.close()
@pytest.mark.asyncio
async def test_error_boundary_testing(self, temp_config, temp_db, mock_update, mock_context):
"""Тест границ ошибок - что происходит на крайних случаях"""
config = Config(temp_config)
user_repo = UserRepository(temp_db)
score_repo = ScoreRepository(temp_db)
user_service = UserService(user_repo, score_repo)
user_handlers = UserHandlers(config, user_service)
try:
# Шаг 1: Тест с None значениями
mock_update.effective_user.id = None
mock_update.effective_user.username = None
await user_handlers.handle_start(mock_update, mock_context)
# Система должна обработать None значения
mock_update.message.reply_text.assert_called()
# Шаг 2: Тест с пустыми строками
mock_update.effective_user.id = 123456789
mock_update.effective_user.username = ""
mock_update.effective_user.first_name = ""
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_start(mock_update, mock_context)
# Шаг 3: Тест с очень длинными значениями
mock_update.effective_user.username = "a" * 1000
mock_update.effective_user.first_name = "b" * 1000
mock_update.message.reply_text.reset_mock()
await user_handlers.handle_start(mock_update, mock_context)
# Система должна корректно обработать все случаи
assert mock_update.message.reply_text.called
finally:
user_repo.close()
score_repo.close()