mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-05-02 02:36:26 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
739 lines
26 KiB
Python
739 lines
26 KiB
Python
from datetime import datetime, timedelta
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.models import User
|
||
from app.services.subscription_auto_purchase_service import auto_purchase_saved_cart_after_topup
|
||
from app.services.subscription_purchase_service import (
|
||
PurchaseDevicesConfig,
|
||
PurchaseOptionsContext,
|
||
PurchasePeriodConfig,
|
||
PurchasePricingResult,
|
||
PurchaseSelection,
|
||
PurchaseServersConfig,
|
||
PurchaseTrafficConfig,
|
||
)
|
||
|
||
|
||
class DummyTexts:
|
||
def t(self, key: str, default: str):
|
||
return default
|
||
|
||
def format_price(self, value: int) -> str:
|
||
return f'{value / 100:.0f} ₽'
|
||
|
||
|
||
async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 42
|
||
user.telegram_id = 4242
|
||
user.balance_kopeks = 200_000
|
||
user.language = 'ru'
|
||
user.subscription = None
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'period_days': 30,
|
||
'countries': ['ru'],
|
||
'traffic_gb': 0,
|
||
'devices': 1,
|
||
}
|
||
|
||
traffic_config = PurchaseTrafficConfig(
|
||
selectable=False,
|
||
mode='fixed',
|
||
options=[],
|
||
default_value=0,
|
||
current_value=0,
|
||
)
|
||
servers_config = PurchaseServersConfig(
|
||
options=[],
|
||
min_selectable=0,
|
||
max_selectable=0,
|
||
default_selection=['ru'],
|
||
)
|
||
devices_config = PurchaseDevicesConfig(
|
||
minimum=1,
|
||
maximum=5,
|
||
default=1,
|
||
current=1,
|
||
price_per_device=0,
|
||
discounted_price_per_device=0,
|
||
price_label='0 ₽',
|
||
)
|
||
|
||
period_config = PurchasePeriodConfig(
|
||
id='days:30',
|
||
days=30,
|
||
months=1,
|
||
label='30 дней',
|
||
base_price=100_000,
|
||
base_price_label='1000 ₽',
|
||
base_price_original=100_000,
|
||
base_price_original_label=None,
|
||
discount_percent=0,
|
||
per_month_price=100_000,
|
||
per_month_price_label='1000 ₽',
|
||
traffic=traffic_config,
|
||
servers=servers_config,
|
||
devices=devices_config,
|
||
)
|
||
|
||
context = PurchaseOptionsContext(
|
||
user=user,
|
||
subscription=None,
|
||
currency='RUB',
|
||
balance_kopeks=user.balance_kopeks,
|
||
periods=[period_config],
|
||
default_period=period_config,
|
||
period_map={'days:30': period_config},
|
||
server_uuid_to_id={'ru': 1},
|
||
payload={},
|
||
)
|
||
|
||
base_pricing = PurchasePricingResult(
|
||
selection=PurchaseSelection(
|
||
period=period_config,
|
||
traffic_value=0,
|
||
servers=['ru'],
|
||
devices=1,
|
||
),
|
||
server_ids=[1],
|
||
server_prices_for_period=[100_000],
|
||
base_original_total=100_000,
|
||
discounted_total=100_000,
|
||
promo_discount_value=0,
|
||
promo_discount_percent=0,
|
||
final_total=100_000,
|
||
months=1,
|
||
details={'servers_individual_prices': [100_000]},
|
||
)
|
||
|
||
class DummyMiniAppService:
|
||
async def build_options(self, db, user):
|
||
return context
|
||
|
||
async def calculate_pricing(self, db, ctx, selection):
|
||
return PurchasePricingResult(
|
||
selection=selection,
|
||
server_ids=base_pricing.server_ids,
|
||
server_prices_for_period=base_pricing.server_prices_for_period,
|
||
base_original_total=base_pricing.base_original_total,
|
||
discounted_total=base_pricing.discounted_total,
|
||
promo_discount_value=base_pricing.promo_discount_value,
|
||
promo_discount_percent=base_pricing.promo_discount_percent,
|
||
final_total=base_pricing.final_total,
|
||
months=base_pricing.months,
|
||
details=base_pricing.details,
|
||
)
|
||
|
||
async def submit_purchase(self, db, prepared_context, pricing):
|
||
return {
|
||
'subscription': MagicMock(),
|
||
'transaction': MagicMock(),
|
||
'was_trial_conversion': False,
|
||
'message': '🎉 Subscription purchased',
|
||
}
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.MiniAppSubscriptionPurchaseService',
|
||
lambda: DummyMiniAppService(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
delete_cart_mock = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.delete_user_cart',
|
||
delete_cart_mock,
|
||
)
|
||
clear_draft_mock = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.clear_subscription_checkout_draft',
|
||
clear_draft_mock,
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_purchase_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
# Мокаем get_user_by_id чтобы вернуть того же user
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_user_by_id',
|
||
AsyncMock(return_value=user),
|
||
)
|
||
|
||
bot = AsyncMock()
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
assert result is True
|
||
delete_cart_mock.assert_awaited_once_with(user.id)
|
||
clear_draft_mock.assert_awaited_once_with(user.id)
|
||
bot.send_message.assert_awaited()
|
||
admin_service_mock.send_subscription_purchase_notification.assert_awaited()
|
||
|
||
|
||
async def test_auto_purchase_saved_cart_after_topup_extension(monkeypatch):
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
|
||
subscription = MagicMock()
|
||
subscription.id = 99
|
||
subscription.is_trial = False
|
||
subscription.status = 'active'
|
||
subscription.end_date = datetime.utcnow()
|
||
subscription.device_limit = 1
|
||
subscription.traffic_limit_gb = 100
|
||
subscription.connected_squads = ['squad-a']
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 7
|
||
user.telegram_id = 7007
|
||
user.balance_kopeks = 200_000
|
||
user.language = 'ru'
|
||
user.subscription = subscription
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'cart_mode': 'extend',
|
||
'subscription_id': subscription.id,
|
||
'period_days': 30,
|
||
'total_price': 31_000,
|
||
'description': 'Продление подписки на 30 дней',
|
||
'device_limit': 2,
|
||
'traffic_limit_gb': 500,
|
||
'squad_uuid': 'squad-b',
|
||
'consume_promo_offer': True,
|
||
}
|
||
|
||
subtract_mock = AsyncMock(return_value=True)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.subtract_user_balance',
|
||
subtract_mock,
|
||
)
|
||
|
||
async def extend_stub(db, current_subscription, days):
|
||
current_subscription.end_date = current_subscription.end_date + timedelta(days=days)
|
||
return current_subscription
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.extend_subscription',
|
||
extend_stub,
|
||
)
|
||
|
||
create_transaction_mock = AsyncMock(return_value=MagicMock())
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.create_transaction',
|
||
create_transaction_mock,
|
||
)
|
||
|
||
service_mock = MagicMock()
|
||
service_mock.update_remnawave_user = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.SubscriptionService',
|
||
lambda: service_mock,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
delete_cart_mock = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.delete_user_cart',
|
||
delete_cart_mock,
|
||
)
|
||
clear_draft_mock = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.clear_subscription_checkout_draft',
|
||
clear_draft_mock,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_extension_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
|
||
# Мок для get_subscription_by_user_id
|
||
monkeypatch.setattr(
|
||
'app.database.crud.subscription.get_subscription_by_user_id',
|
||
AsyncMock(return_value=subscription),
|
||
)
|
||
|
||
bot = AsyncMock()
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
assert result is True
|
||
subtract_mock.assert_awaited_once_with(
|
||
db_session,
|
||
user,
|
||
cart_data['total_price'],
|
||
cart_data['description'],
|
||
consume_promo_offer=True,
|
||
)
|
||
assert subscription.device_limit == 2
|
||
assert subscription.traffic_limit_gb == 500
|
||
assert 'squad-b' in subscription.connected_squads
|
||
delete_cart_mock.assert_awaited_once_with(user.id)
|
||
clear_draft_mock.assert_awaited_once_with(user.id)
|
||
admin_service_mock.send_subscription_extension_notification.assert_awaited()
|
||
bot.send_message.assert_awaited()
|
||
service_mock.update_remnawave_user.assert_awaited()
|
||
create_transaction_mock.assert_awaited()
|
||
|
||
|
||
async def test_auto_purchase_trial_preserved_on_insufficient_balance(monkeypatch):
|
||
"""Тест: триал сохраняется, если не хватает денег для автопокупки"""
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
|
||
subscription = MagicMock()
|
||
subscription.id = 123
|
||
subscription.is_trial = True # Триальная подписка!
|
||
subscription.status = 'active'
|
||
subscription.end_date = datetime.utcnow() + timedelta(days=2) # Осталось 2 дня
|
||
subscription.device_limit = 1
|
||
subscription.traffic_limit_gb = 10
|
||
subscription.connected_squads = []
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 99
|
||
user.telegram_id = 9999
|
||
# ИСПРАВЛЕНО: Баланс достаточный для первой проверки (строка 243),
|
||
# но subtract_user_balance вернёт False (симуляция неудачи списания)
|
||
user.balance_kopeks = 60_000
|
||
user.language = 'ru'
|
||
user.subscription = subscription
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'cart_mode': 'extend',
|
||
'subscription_id': subscription.id,
|
||
'period_days': 30,
|
||
'total_price': 50_000,
|
||
'description': 'Продление на 30 дней',
|
||
'device_limit': 1,
|
||
'traffic_limit_gb': 100,
|
||
'squad_uuid': None,
|
||
'consume_promo_offer': False,
|
||
}
|
||
|
||
# Mock: недостаточно денег, списание не удалось
|
||
subtract_mock = AsyncMock(return_value=False)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.subtract_user_balance',
|
||
subtract_mock,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_local_datetime',
|
||
lambda dt, fmt: dt.strftime(fmt) if dt else '',
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_extension_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
|
||
# Мок для get_subscription_by_user_id
|
||
monkeypatch.setattr(
|
||
'app.database.crud.subscription.get_subscription_by_user_id',
|
||
AsyncMock(return_value=subscription),
|
||
)
|
||
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
bot = AsyncMock()
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
# Проверки
|
||
assert result is False # Автопокупка не удалась
|
||
assert subscription.is_trial is True # ТРИАЛ СОХРАНЁН!
|
||
subtract_mock.assert_awaited_once()
|
||
|
||
|
||
async def test_auto_purchase_trial_converted_after_successful_extension(monkeypatch):
|
||
"""Тест: триал конвертируется в платную подписку ТОЛЬКО после успешного продления"""
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
|
||
subscription = MagicMock()
|
||
subscription.id = 456
|
||
subscription.is_trial = True # Триальная подписка!
|
||
subscription.status = 'active'
|
||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
||
subscription.device_limit = 1
|
||
subscription.traffic_limit_gb = 10
|
||
subscription.connected_squads = []
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 88
|
||
user.telegram_id = 8888
|
||
user.balance_kopeks = 200_000 # Достаточно денег
|
||
user.language = 'ru'
|
||
user.subscription = subscription
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'cart_mode': 'extend',
|
||
'subscription_id': subscription.id,
|
||
'period_days': 30,
|
||
'total_price': 100_000,
|
||
'description': 'Продление на 30 дней',
|
||
'device_limit': 2,
|
||
'traffic_limit_gb': 500,
|
||
'squad_uuid': None,
|
||
'consume_promo_offer': False,
|
||
}
|
||
|
||
# Mock: деньги списались успешно
|
||
subtract_mock = AsyncMock(return_value=True)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.subtract_user_balance',
|
||
subtract_mock,
|
||
)
|
||
|
||
# Mock: продление успешно
|
||
async def extend_stub(db, current_subscription, days):
|
||
current_subscription.end_date = current_subscription.end_date + timedelta(days=days)
|
||
return current_subscription
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.extend_subscription',
|
||
extend_stub,
|
||
)
|
||
|
||
create_transaction_mock = AsyncMock(return_value=MagicMock())
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.create_transaction',
|
||
create_transaction_mock,
|
||
)
|
||
|
||
service_mock = MagicMock()
|
||
service_mock.update_remnawave_user = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.SubscriptionService',
|
||
lambda: service_mock,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.delete_user_cart',
|
||
AsyncMock(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.clear_subscription_checkout_draft',
|
||
AsyncMock(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
# ИСПРАВЛЕНО: Добавлен мок для format_local_datetime
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_local_datetime',
|
||
lambda dt, fmt: dt.strftime(fmt) if dt else '',
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_extension_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
|
||
# Мок для get_subscription_by_user_id
|
||
monkeypatch.setattr(
|
||
'app.database.crud.subscription.get_subscription_by_user_id',
|
||
AsyncMock(return_value=subscription),
|
||
)
|
||
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
db_session.commit = AsyncMock() # Важно! Отслеживаем commit
|
||
db_session.refresh = AsyncMock() # ИСПРАВЛЕНО: Добавлен мок для refresh
|
||
bot = AsyncMock()
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
# Проверки
|
||
assert result is True # Автопокупка успешна
|
||
assert subscription.is_trial is False # ТРИАЛ КОНВЕРТИРОВАН!
|
||
assert subscription.status == 'active'
|
||
db_session.commit.assert_awaited() # Commit был вызван
|
||
|
||
|
||
async def test_auto_purchase_trial_preserved_on_extension_failure(monkeypatch):
|
||
"""Тест: триал НЕ конвертируется и вызывается rollback при ошибке в extend_subscription"""
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
|
||
subscription = MagicMock()
|
||
subscription.id = 789
|
||
subscription.is_trial = True # Триальная подписка!
|
||
subscription.status = 'active'
|
||
subscription.end_date = datetime.utcnow() + timedelta(days=3)
|
||
subscription.device_limit = 1
|
||
subscription.traffic_limit_gb = 10
|
||
subscription.connected_squads = []
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 77
|
||
user.telegram_id = 7777
|
||
user.balance_kopeks = 200_000 # Достаточно денег
|
||
user.language = 'ru'
|
||
user.subscription = subscription
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'cart_mode': 'extend',
|
||
'subscription_id': subscription.id,
|
||
'period_days': 30,
|
||
'total_price': 100_000,
|
||
'description': 'Продление на 30 дней',
|
||
'device_limit': 1,
|
||
'traffic_limit_gb': 100,
|
||
'squad_uuid': None,
|
||
'consume_promo_offer': False,
|
||
}
|
||
|
||
# Mock: деньги списались успешно
|
||
subtract_mock = AsyncMock(return_value=True)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.subtract_user_balance',
|
||
subtract_mock,
|
||
)
|
||
|
||
# Mock: extend_subscription выбрасывает ошибку!
|
||
async def extend_error(db, current_subscription, days):
|
||
raise Exception('Database connection error')
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.extend_subscription',
|
||
extend_error,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
|
||
# ИСПРАВЛЕНО: Добавлены недостающие моки
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_local_datetime',
|
||
lambda dt, fmt: dt.strftime(fmt) if dt else '',
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_extension_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
|
||
# Мок для get_subscription_by_user_id
|
||
monkeypatch.setattr(
|
||
'app.database.crud.subscription.get_subscription_by_user_id',
|
||
AsyncMock(return_value=subscription),
|
||
)
|
||
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
db_session.rollback = AsyncMock() # Важно! Отслеживаем rollback
|
||
db_session.refresh = AsyncMock() # ИСПРАВЛЕНО: Добавлен мок для refresh
|
||
bot = AsyncMock()
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
# Проверки
|
||
assert result is False # Автопокупка не удалась
|
||
assert subscription.is_trial is True # ТРИАЛ СОХРАНЁН!
|
||
db_session.rollback.assert_awaited() # ROLLBACK БЫЛ ВЫЗВАН!
|
||
|
||
|
||
async def test_auto_purchase_trial_remaining_days_transferred(monkeypatch):
|
||
"""Тест: остаток триала переносится на платную подписку при TRIAL_ADD_REMAINING_DAYS_TO_PAID=True"""
|
||
monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True)
|
||
monkeypatch.setattr(settings, 'TRIAL_ADD_REMAINING_DAYS_TO_PAID', True) # Включено!
|
||
|
||
now = datetime.utcnow()
|
||
trial_end = now + timedelta(days=2) # Осталось 2 дня триала
|
||
|
||
subscription = MagicMock()
|
||
subscription.id = 321
|
||
subscription.is_trial = True
|
||
subscription.status = 'active'
|
||
subscription.end_date = trial_end
|
||
subscription.start_date = now - timedelta(days=1) # Триал начался вчера
|
||
subscription.device_limit = 1
|
||
subscription.traffic_limit_gb = 10
|
||
subscription.connected_squads = []
|
||
|
||
user = MagicMock(spec=User)
|
||
user.id = 66
|
||
user.telegram_id = 6666
|
||
user.balance_kopeks = 200_000
|
||
user.language = 'ru'
|
||
user.subscription = subscription
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
|
||
cart_data = {
|
||
'cart_mode': 'extend',
|
||
'subscription_id': subscription.id,
|
||
'period_days': 30, # Покупает 30 дней
|
||
'total_price': 100_000,
|
||
'description': 'Продление на 30 дней',
|
||
'device_limit': 1,
|
||
'traffic_limit_gb': 100,
|
||
'squad_uuid': None,
|
||
'consume_promo_offer': False,
|
||
}
|
||
|
||
subtract_mock = AsyncMock(return_value=True)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.subtract_user_balance',
|
||
subtract_mock,
|
||
)
|
||
|
||
# Mock: extend_subscription с логикой переноса бонусных дней
|
||
# Имитируем нашу новую логику из extend_subscription()
|
||
async def extend_with_bonus(db, current_subscription, days):
|
||
# Вычисляем бонусные дни (как в нашем коде)
|
||
bonus_days = 0
|
||
if current_subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID:
|
||
if current_subscription.end_date and current_subscription.end_date > now:
|
||
remaining = current_subscription.end_date - now
|
||
if remaining.total_seconds() > 0:
|
||
bonus_days = max(0, remaining.days)
|
||
|
||
total_days = days + bonus_days
|
||
current_subscription.end_date = current_subscription.end_date + timedelta(days=total_days)
|
||
return current_subscription
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.extend_subscription',
|
||
extend_with_bonus,
|
||
)
|
||
|
||
create_transaction_mock = AsyncMock(return_value=MagicMock())
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.create_transaction',
|
||
create_transaction_mock,
|
||
)
|
||
|
||
service_mock = MagicMock()
|
||
service_mock.update_remnawave_user = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.SubscriptionService',
|
||
lambda: service_mock,
|
||
)
|
||
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||
AsyncMock(return_value=cart_data),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.user_cart_service.delete_user_cart',
|
||
AsyncMock(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.clear_subscription_checkout_draft',
|
||
AsyncMock(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.get_texts',
|
||
lambda lang: DummyTexts(),
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_period_description',
|
||
lambda days, lang: f'{days} дней',
|
||
)
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.format_local_datetime',
|
||
lambda dt, fmt: dt.strftime(fmt),
|
||
)
|
||
|
||
admin_service_mock = MagicMock()
|
||
admin_service_mock.send_subscription_extension_notification = AsyncMock()
|
||
monkeypatch.setattr(
|
||
'app.services.subscription_auto_purchase_service.AdminNotificationService',
|
||
lambda bot: admin_service_mock,
|
||
)
|
||
|
||
# Мок для get_subscription_by_user_id
|
||
monkeypatch.setattr(
|
||
'app.database.crud.subscription.get_subscription_by_user_id',
|
||
AsyncMock(return_value=subscription),
|
||
)
|
||
|
||
db_session = AsyncMock(spec=AsyncSession)
|
||
db_session.commit = AsyncMock()
|
||
db_session.refresh = AsyncMock() # ИСПРАВЛЕНО: Добавлен мок для refresh
|
||
bot = AsyncMock()
|
||
|
||
result = await auto_purchase_saved_cart_after_topup(db_session, user, bot=bot)
|
||
|
||
# Проверки
|
||
assert result is True
|
||
assert subscription.is_trial is False # Триал конвертирован
|
||
|
||
# Проверяем, что подписка продлена на 32 дня (30 + 2 бонусных)
|
||
# end_date должна быть примерно на 32 дня от оригинального trial_end
|
||
trial_end + timedelta(days=32) # trial_end + (30 + 2)
|
||
actual_delta = (subscription.end_date - trial_end).days
|
||
assert actual_delta == 32, f'Expected 32 days extension (30 + 2 bonus), got {actual_delta}'
|