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

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,595 @@
"""
Комплексные интеграционные тесты для платежей.
Тестирование полного цикла донатов: создание платежа -> обработка -> начисление очков -> достижения.
Проверяет взаимодействие: 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()