""" Комплексные интеграционные тесты для платежей. Тестирование полного цикла донатов: создание платежа -> обработка -> начисление очков -> достижения. Проверяет взаимодействие: handlers -> donation_service -> payment_providers -> repositories. """ import pytest import tempfile import os from unittest.mock import Mock, AsyncMock, patch from datetime import datetime # Добавляем корневую директорию в путь import sys 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 from services.donation_service import DonationService from services.payment_provider import StripePaymentProvider, YooKassaPaymentProvider from handlers.user_handlers import UserHandlers from database import UserRepository, ScoreRepository, PaymentRepository class TestPaymentsIntegration: """Комплексные интеграционные тесты системы платежей""" @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] 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" SBP_API_KEY = "sbp_test_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 = "donator_user" user.first_name = "Donator" user.last_name = "Test" 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_donation_full_flow_integration(self, temp_config, temp_db, mock_update, mock_context): """Интеграционный тест полного цикла доната: handler -> service -> repository""" 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) user_handlers = UserHandlers(config, user_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 ) # Шаг 2: Инициируем донат через сервис donation_amount = 500.0 success = await user_service.add_donation(mock_update.effective_user.id, donation_amount) # Шаг 3: Проверяем, что донат прошел успешно assert success # Шаг 4: Проверяем начисление очков (500 рублей = 5 очков) expected_points = int(donation_amount // 100) user_score = score_repo.get_total_score(mock_update.effective_user.id) assert user_score == expected_points # Шаг 5: Проверяем обновление профиля пользователя 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.reputation == expected_points # Шаг 6: Проверяем достижения (должно появиться "Первый донат") user_achievements = await user_service.get_user_achievements(mock_update.effective_user.id) achievement_names = [] for achievement_id, unlocked_at in user_achievements: achievement_data = user_repo.get_all_achievements() for ach in achievement_data: if ach['id'] == achievement_id: achievement_names.append(ach['name']) break assert "Первый донат" in achievement_names # Шаг 7: Проверяем статистику донатов total_donations = await user_service.get_total_donations(mock_update.effective_user.id, 2025) assert total_donations == donation_amount finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_multiple_donations_integration(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: Несколько донатов donations = [300.0, 500.0, 1000.0] # 3, 5, 10 очков соответственно for amount in donations: success = await user_service.add_donation(mock_update.effective_user.id, amount) assert success # Шаг 3: Проверяем итоговые очки (3 + 5 + 10 = 18) total_score = score_repo.get_total_score(mock_update.effective_user.id) expected_total = sum(int(amount // 100) for amount in donations) assert total_score == expected_total # Шаг 4: Проверяем достижения (должен появиться "Меценат") user_achievements = await user_service.get_user_achievements(mock_update.effective_user.id) achievement_names = [] for achievement_id, unlocked_at in user_achievements: achievement_data = user_repo.get_all_achievements() for ach in achievement_data: if ach['id'] == achievement_id: achievement_names.append(ach['name']) break assert "Первый донат" in achievement_names assert "Меценат" in achievement_names # Шаг 5: Проверяем общую сумму донатов total_donations = await user_service.get_total_donations(mock_update.effective_user.id, 2025) assert total_donations == sum(donations) finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_ranks_integration(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) try: # Шаг 1: Создаем пользователя 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.rank == "Новичок" # Шаг 2: Большой донат (1000 рублей = 10 очков, должно дать ранг) donation_amount = 1000.0 success = await user_service.add_donation(mock_update.effective_user.id, donation_amount) assert success # Шаг 3: Проверяем обновление ранга updated_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 ) # При 10+ очках ранг должен измениться assert updated_profile.reputation >= 10 # Ранг может зависеть от логики, но очки должны быть начислены # Шаг 4: Еще один большой донат для достижения высокого ранга second_donation = 2000.0 success2 = await user_service.add_donation(mock_update.effective_user.id, second_donation) assert success2 # Шаг 5: Финальная проверка final_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 ) expected_total_points = 10 + 20 # 1000/100 + 2000/100 assert final_profile.reputation == expected_total_points finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_payment_providers_integration(self, temp_config, temp_db, mock_update, mock_context): """Тест интеграции с платежными провайдерами""" config = Config(temp_config) payment_repo = PaymentRepository(temp_db) try: # Шаг 1: Тестируем Stripe провайдер stripe_provider = StripePaymentProvider( api_key=config.api_keys.stripe_secret, webhook_secret="whsec_test_webhook" ) # Мокаем создание платежа в Stripe with patch.object(stripe_provider, '_stripe') as mock_stripe: mock_payment_intent = Mock() mock_payment_intent.id = "pi_test_123" mock_payment_intent.client_secret = "pi_test_secret" mock_stripe.PaymentIntent.create.return_value = mock_payment_intent payment_intent = stripe_provider.create_payment( amount=500.0, currency="RUB", user_id=mock_update.effective_user.id, metadata={"description": "Test donation"} ) assert payment_intent.payment_id == "pi_test_123" # Шаг 2: Тестируем YooKassa провайдер yookassa_provider = YooKassaPaymentProvider( shop_id=config.api_keys.yookassa_shop_id, secret_key=config.api_keys.yookassa_secret ) # Мокаем создание платежа в YooKassa with patch.object(yookassa_provider, '_yookassa') as mock_yookassa: mock_payment = Mock() mock_payment.id = "payment_test_456" mock_payment.confirmation.confirmation_url = "https://test.payment.url" mock_yookassa.Payment.create.return_value = mock_payment payment_intent = yookassa_provider.create_payment( amount=300.0, currency="RUB", user_id=mock_update.effective_user.id, metadata={"description": "Test YooKassa donation"} ) assert payment_intent.payment_id == "payment_test_456" finally: payment_repo.close() @pytest.mark.asyncio async def test_donation_webhook_processing_integration(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 ) # Создаем платеж в базе данных payment_data = { 'user_id': mock_update.effective_user.id, 'amount': 500.0, 'currency': 'RUB', 'provider': 'stripe', 'external_id': 'pi_test_webhook', 'status': 'pending', 'created_at': datetime.now() } payment_repo.create_payment(payment_data) # Шаг 2: Имитируем успешный вебхук webhook_data = { 'id': 'evt_test_webhook', 'type': 'payment_intent.succeeded', 'data': { 'object': { 'id': 'pi_test_webhook', 'amount': 50000, # в копейках/центах 'currency': 'rub' } } } # Мокаем обработку вебхука success = await donation_service.process_payment_webhook('stripe', webhook_data) assert success # Шаг 3: Проверяем, что платеж обновлен payment = payment_repo.get_payment_by_external_id('pi_test_webhook') assert payment is not None assert payment['status'] == 'completed' # Шаг 4: Проверяем начисление очков пользователю user_score = score_repo.get_total_score(mock_update.effective_user.id) assert user_score == 5 # 500 рублей = 5 очков finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_statistics_integration(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: Создаем несколько пользователей и донатов users = [123456789, 987654321, 555666777] donations = [300.0, 500.0, 1000.0] for user_id in users: await user_service.get_or_create_user(user_id, f"user_{user_id}", "Test", "User") for i, amount in enumerate(donations): user_id = users[i % len(users)] await user_service.add_donation(user_id, amount) # Шаг 2: Получаем статистику платежей stats = donation_service.get_payment_statistics() # Шаг 3: Проверяем статистику assert isinstance(stats, dict) assert 'total_payments' in stats assert 'total_amount' in stats assert 'successful_payments' in stats # Проверяем, что статистика корректна assert stats['total_payments'] >= 3 assert stats['total_amount'] >= sum(donations) finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_error_handling_integration(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 processing error")) # Шаг 3: Пытаемся сделать донат success = await user_service.add_donation(mock_update.effective_user.id, 500.0) assert not success # Шаг 4: Восстанавливаем функцию и тестируем успешный донат user_service.add_donation = original_add_donation success_retry = await user_service.add_donation(mock_update.effective_user.id, 500.0) assert success_retry # Шаг 5: Проверяем, что несмотря на ошибку, успешный донат прошел user_score = score_repo.get_total_score(mock_update.effective_user.id) assert user_score == 5 finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_duplicate_prevention_integration(self, temp_config, temp_db, mock_update, mock_context): """Тест предотвращения дублированных платежей""" config = Config(temp_config) payment_repo = PaymentRepository(temp_db) donation_service = DonationService(payment_repo, config.api_keys) try: # Шаг 1: Создаем платеж payment_data = { 'user_id': mock_update.effective_user.id, 'amount': 500.0, 'currency': 'RUB', 'provider': 'stripe', 'external_id': 'pi_test_duplicate', 'status': 'pending', 'created_at': datetime.now() } payment_repo.create_payment(payment_data) # Шаг 2: Проверяем дублирование is_duplicate = donation_service._validate_donation_data( mock_update.effective_user.id, 500.0, 'stripe' ) # В реальности проверка дублирования может работать иначе, # но метод должен существовать assert hasattr(donation_service, '_validate_donation_data') finally: payment_repo.close() @pytest.mark.asyncio async def test_donation_user_balance_integration(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: Создаем пользователя 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 ) initial_balance = profile.reputation # Шаг 2: Имитируем успешный платеж await donation_service._update_user_balance(mock_update.effective_user.id, 300.0) # Шаг 3: Проверяем обновление баланса updated_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 updated_profile.reputation == initial_balance + 3 # 300 рублей = 3 очка finally: user_repo.close() score_repo.close() payment_repo.close() @pytest.mark.asyncio async def test_donation_payment_cancellation_integration(self, temp_config, temp_db, mock_update, mock_context): """Тест отмены платежей""" config = Config(temp_config) payment_repo = PaymentRepository(temp_db) donation_service = DonationService(payment_repo, config.api_keys) try: # Шаг 1: Создаем платеж payment_data = { 'user_id': mock_update.effective_user.id, 'amount': 500.0, 'currency': 'RUB', 'provider': 'stripe', 'external_id': 'pi_test_cancel', 'status': 'pending', 'created_at': datetime.now() } created_payment = payment_repo.create_payment(payment_data) payment_id = created_payment['id'] # Шаг 2: Отменяем платеж cancelled = donation_service.cancel_payment(payment_id, mock_update.effective_user.id) assert cancelled # Шаг 3: Проверяем статус платежа payment = payment_repo.get_payment_by_id(payment_id) assert payment['status'] == 'cancelled' finally: payment_repo.close()