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