mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
573 lines
26 KiB
Python
573 lines
26 KiB
Python
"""
|
|
Комплексные интеграционные тесты обработки ошибок между слоями архитектуры.
|
|
Тестирование: 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() |