mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 11:50:27 +00:00
- Add JWT authentication for cabinet users - Add Telegram WebApp authentication - Add subscription management endpoints - Add balance and transactions endpoints - Add referral system endpoints - Add tickets support for cabinet - Add webhooks and websocket for real-time updates - Add email verification service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
739 lines
26 KiB
Python
739 lines
26 KiB
Python
import pytest
|
||
from datetime import datetime, timedelta
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
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,
|
||
)
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
|
||
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
|
||
expected_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}"
|