mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
2) У промогрупп появится приоритет 3) У пользователя может быть несколько промогрупп, но влиять будет только с наивысшим приоритетом 4) Промокоды с промогруппой 5) При выводе пользователей с промогруппой будет также выводиться ссылка на каждого. Можно будет отследить сливы промокодов "для своих". Я в целом это добавлю во все места, где пользователь выводится в админке 6) Исправить баг исчезновения триалки при пополнении 7) Исправить падающие тесты и добавить новых 8) Трафик: 0 ГБ в тестовой подписке исправить на Трафик: Безлимит 9) При попытке изменить промогруппу "Пользователь не найден" - исправил
395 lines
18 KiB
Python
395 lines
18 KiB
Python
import pytest
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.types import (
|
||
CallbackQuery,
|
||
User as TgUser,
|
||
Message,
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
)
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from app.handlers.subscription.purchase import save_cart_and_redirect_to_topup, return_to_saved_cart, clear_saved_cart
|
||
from app.handlers.subscription.autopay import handle_subscription_cancel
|
||
from app.database.models import User, Subscription
|
||
|
||
@pytest.fixture
|
||
def mock_callback_query():
|
||
callback = AsyncMock(spec=CallbackQuery)
|
||
callback.message = AsyncMock(spec=Message)
|
||
callback.message.edit_text = AsyncMock()
|
||
callback.answer = AsyncMock()
|
||
callback.data = "subscription_confirm"
|
||
return callback
|
||
|
||
@pytest.fixture
|
||
def mock_user():
|
||
user = AsyncMock(spec=User)
|
||
user.id = 12345
|
||
user.telegram_id = 12345
|
||
user.language = "ru"
|
||
user.balance_kopeks = 10000
|
||
user.subscription = None
|
||
user.has_had_paid_subscription = False
|
||
user.promo_group_id = None
|
||
user.get_primary_promo_group = MagicMock(return_value=None)
|
||
user.get_promo_discount = MagicMock(return_value=0)
|
||
user.promo_offer_discount_percent = 0
|
||
user.promo_offer_discount_expires_at = None
|
||
return user
|
||
|
||
@pytest.fixture
|
||
def mock_db():
|
||
db = AsyncMock(spec=AsyncSession)
|
||
return db
|
||
|
||
@pytest.fixture
|
||
def mock_state():
|
||
state = AsyncMock(spec=FSMContext)
|
||
state.get_data = AsyncMock(return_value={
|
||
'period_days': 30,
|
||
'countries': ['ru'],
|
||
'devices': 2,
|
||
'traffic_gb': 10,
|
||
'total_price': 50000
|
||
})
|
||
state.set_data = AsyncMock()
|
||
state.update_data = AsyncMock()
|
||
state.set_state = AsyncMock()
|
||
state.clear = AsyncMock()
|
||
return state
|
||
|
||
async def test_save_cart_and_redirect_to_topup(mock_callback_query, mock_state, mock_user, mock_db):
|
||
"""Тест сохранения корзины и перенаправления к пополнению"""
|
||
# Мокаем все зависимости
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.subscription.purchase.get_payment_methods_keyboard_with_cart') as mock_keyboard_func, \
|
||
patch('app.localization.texts.get_texts') as mock_get_texts:
|
||
|
||
# Подготовим моки
|
||
mock_cart_service.save_user_cart = AsyncMock(return_value=True)
|
||
mock_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="✅", callback_data="confirm")]]
|
||
)
|
||
mock_keyboard_func.return_value = mock_keyboard
|
||
|
||
# Подготовим тексты
|
||
mock_texts = AsyncMock()
|
||
mock_texts.format_price = lambda x: f"{x/100:.0f} ₽"
|
||
mock_get_texts.return_value = mock_texts
|
||
|
||
missing_amount = 40000 # 50000 - 10000 = 40000
|
||
|
||
# Вызываем функцию
|
||
await save_cart_and_redirect_to_topup(mock_callback_query, mock_state, mock_user, missing_amount)
|
||
|
||
# Проверяем, что данные были сохранены в корзину
|
||
mock_cart_service.save_user_cart.assert_called_once()
|
||
args, kwargs = mock_cart_service.save_user_cart.call_args
|
||
saved_user_id, saved_cart_data = args
|
||
|
||
assert saved_user_id == mock_user.id
|
||
assert saved_cart_data['period_days'] == 30
|
||
assert saved_cart_data['countries'] == ['ru']
|
||
assert saved_cart_data['devices'] == 2
|
||
assert saved_cart_data['traffic_gb'] == 10
|
||
assert saved_cart_data['total_price'] == 50000
|
||
assert saved_cart_data['saved_cart'] is True
|
||
assert saved_cart_data['missing_amount'] == missing_amount
|
||
assert saved_cart_data['return_to_cart'] is True
|
||
assert saved_cart_data['user_id'] == mock_user.id
|
||
|
||
# Проверяем, что сообщение было отредактировано
|
||
mock_callback_query.message.edit_text.assert_called_once()
|
||
|
||
# В этой функции нет вызова callback.answer()
|
||
# mock_callback_query.answer не должен быть вызван
|
||
mock_callback_query.answer.assert_not_called()
|
||
|
||
async def test_return_to_saved_cart_success(mock_callback_query, mock_state, mock_user, mock_db):
|
||
"""Тест возврата к сохраненной корзине с достаточным балансом"""
|
||
# Подготовим данные корзины
|
||
cart_data = {
|
||
'period_days': 30,
|
||
'countries': ['ru', 'us'],
|
||
'devices': 3,
|
||
'traffic_gb': 20,
|
||
'total_price': 30000, # Меньше, чем баланс пользователя (50000)
|
||
'saved_cart': True,
|
||
'user_id': mock_user.id
|
||
}
|
||
|
||
# Мокаем все зависимости
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.subscription.purchase._get_available_countries') as mock_get_countries, \
|
||
patch('app.handlers.subscription.purchase.format_period_description') as mock_format_period, \
|
||
patch('app.localization.texts.get_texts') as mock_get_texts, \
|
||
patch('app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart') as mock_keyboard_func, \
|
||
patch('app.handlers.subscription.purchase._prepare_subscription_summary') as mock_prepare_summary:
|
||
|
||
# Подготовим моки
|
||
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
|
||
mock_cart_service.save_user_cart = AsyncMock(return_value=True)
|
||
mock_prepare_summary.return_value = ("summary", {})
|
||
mock_get_countries.return_value = [{'uuid': 'ru', 'name': 'Russia'}, {'uuid': 'us', 'name': 'USA'}]
|
||
mock_format_period.return_value = "30 дней"
|
||
mock_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="✅", callback_data="confirm")]]
|
||
)
|
||
mock_keyboard_func.return_value = mock_keyboard
|
||
|
||
# Подготовим тексты
|
||
mock_texts = AsyncMock()
|
||
mock_texts.format_price = lambda x: f"{x/100:.0f} ₽"
|
||
mock_get_texts.return_value = mock_texts
|
||
|
||
# Увеличиваем баланс пользователя, чтобы его хватило
|
||
mock_user.balance_kopeks = 50000
|
||
|
||
# Вызываем функцию
|
||
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
# Проверяем, что корзина была загружена
|
||
mock_cart_service.get_user_cart.assert_called_once_with(mock_user.id)
|
||
|
||
# Проверяем, что сообщение было отредактировано
|
||
mock_callback_query.message.edit_text.assert_called_once()
|
||
|
||
# В успешном сценарии вызывается callback.answer()
|
||
mock_callback_query.answer.assert_called_once()
|
||
|
||
|
||
async def test_return_to_saved_cart_skips_edit_when_message_matches(
|
||
mock_callback_query,
|
||
mock_state,
|
||
mock_user,
|
||
mock_db,
|
||
):
|
||
cart_data = {
|
||
'period_days': 60,
|
||
'countries': ['ru', 'us'],
|
||
'devices': 3,
|
||
'traffic_gb': 40,
|
||
'total_price': 44000,
|
||
'saved_cart': True,
|
||
'user_id': mock_user.id,
|
||
}
|
||
|
||
confirm_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="Подтвердить", callback_data="confirm")]]
|
||
)
|
||
existing_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="Подтвердить", callback_data="confirm")]]
|
||
)
|
||
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.subscription.purchase._get_available_countries') as mock_get_countries, \
|
||
patch('app.handlers.subscription.purchase.format_period_description') as mock_format_period, \
|
||
patch('app.localization.texts.get_texts') as mock_get_texts, \
|
||
patch('app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart') as mock_keyboard_func, \
|
||
patch('app.handlers.subscription.purchase.settings') as mock_settings:
|
||
|
||
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
|
||
mock_cart_service.save_user_cart = AsyncMock()
|
||
mock_get_countries.return_value = [
|
||
{'uuid': 'ru', 'name': 'Russia'},
|
||
{'uuid': 'us', 'name': 'USA'},
|
||
]
|
||
mock_format_period.return_value = "60 дней"
|
||
mock_keyboard_func.return_value = confirm_keyboard
|
||
|
||
mock_texts = AsyncMock()
|
||
mock_texts.format_price = lambda x: f"{x/100:.0f} ₽"
|
||
mock_get_texts.return_value = mock_texts
|
||
|
||
mock_settings.is_devices_selection_enabled.return_value = True
|
||
mock_settings.is_traffic_fixed.return_value = False
|
||
|
||
mock_user.balance_kopeks = 50000
|
||
|
||
summary_text = (
|
||
"🛒 Восстановленная корзина\n\n"
|
||
"📅 Период: 60 дней\n"
|
||
"📊 Трафик: 40 ГБ\n"
|
||
"🌍 Страны: Russia, USA\n"
|
||
"📱 Устройства: 3\n\n"
|
||
"💎 Общая стоимость: 440 ₽\n\n"
|
||
"Подтверждаете покупку?"
|
||
)
|
||
|
||
mock_callback_query.message.text = summary_text
|
||
mock_callback_query.message.reply_markup = existing_keyboard
|
||
|
||
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
mock_callback_query.message.edit_text.assert_not_called()
|
||
mock_callback_query.answer.assert_called_once_with("✅ Корзина восстановлена!")
|
||
mock_state.set_data.assert_called_once_with(cart_data)
|
||
mock_state.set_state.assert_called_once()
|
||
mock_cart_service.save_user_cart.assert_not_called()
|
||
|
||
|
||
async def test_return_to_saved_cart_normalizes_devices_when_disabled(
|
||
mock_callback_query,
|
||
mock_state,
|
||
mock_user,
|
||
mock_db,
|
||
):
|
||
cart_data = {
|
||
'period_days': 30,
|
||
'countries': ['ru', 'us'],
|
||
'devices': 5,
|
||
'traffic_gb': 20,
|
||
'total_price': 45000,
|
||
'total_devices_price': 15000,
|
||
'saved_cart': True,
|
||
'user_id': mock_user.id,
|
||
}
|
||
|
||
sanitized_summary_data = {
|
||
'period_days': 30,
|
||
'countries': ['ru', 'us'],
|
||
'devices': 3,
|
||
'traffic_gb': 20,
|
||
'total_price': 30000,
|
||
'total_devices_price': 0,
|
||
}
|
||
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.subscription.purchase._get_available_countries') as mock_get_countries, \
|
||
patch('app.handlers.subscription.purchase.format_period_description') as mock_format_period, \
|
||
patch('app.localization.texts.get_texts') as mock_get_texts, \
|
||
patch('app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart') as mock_keyboard_func, \
|
||
patch('app.handlers.subscription.purchase.settings') as mock_settings, \
|
||
patch('app.handlers.subscription.pricing._prepare_subscription_summary', new=AsyncMock(return_value=("ignored", sanitized_summary_data))):
|
||
|
||
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
|
||
mock_cart_service.save_user_cart = AsyncMock()
|
||
mock_get_countries.return_value = [{'uuid': 'ru', 'name': 'Russia'}, {'uuid': 'us', 'name': 'USA'}]
|
||
mock_format_period.return_value = "30 дней"
|
||
mock_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="✅", callback_data="confirm")]]
|
||
)
|
||
mock_keyboard_func.return_value = mock_keyboard
|
||
|
||
mock_texts = AsyncMock()
|
||
mock_texts.format_price = lambda x: f"{x/100:.0f} ₽"
|
||
mock_texts.t = lambda key, default=None: default or ""
|
||
mock_get_texts.return_value = mock_texts
|
||
|
||
mock_settings.is_devices_selection_enabled.return_value = False
|
||
mock_settings.DEFAULT_DEVICE_LIMIT = 3
|
||
mock_settings.is_traffic_fixed.return_value = False
|
||
mock_settings.get_fixed_traffic_limit.return_value = 0
|
||
|
||
mock_user.balance_kopeks = 60000
|
||
|
||
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
mock_cart_service.save_user_cart.assert_called_once()
|
||
_, saved_payload = mock_cart_service.save_user_cart.call_args[0]
|
||
assert saved_payload['devices'] == 3
|
||
assert saved_payload['total_price'] == 30000
|
||
assert saved_payload['saved_cart'] is True
|
||
|
||
mock_state.set_data.assert_called_once()
|
||
normalized_data = mock_state.set_data.call_args[0][0]
|
||
assert normalized_data['devices'] == 3
|
||
assert normalized_data['total_price'] == 30000
|
||
assert normalized_data['saved_cart'] is True
|
||
|
||
edited_text = mock_callback_query.message.edit_text.call_args[0][0]
|
||
assert "📱" not in edited_text
|
||
|
||
mock_callback_query.answer.assert_called_once()
|
||
|
||
async def test_return_to_saved_cart_insufficient_funds(mock_callback_query, mock_state, mock_user, mock_db):
|
||
"""Тест возврата к сохраненной корзине с недостаточным балансом"""
|
||
# Подготовим данные корзины
|
||
cart_data = {
|
||
'period_days': 30,
|
||
'countries': ['ru', 'us'],
|
||
'devices': 3,
|
||
'traffic_gb': 20,
|
||
'total_price': 50000, # Больше, чем баланс пользователя (10000)
|
||
'saved_cart': True,
|
||
'user_id': mock_user.id
|
||
}
|
||
|
||
# Мокаем все зависимости
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.localization.texts.get_texts') as mock_get_texts, \
|
||
patch('app.handlers.subscription.purchase.get_insufficient_balance_keyboard_with_cart') as mock_keyboard_func:
|
||
|
||
# Подготовим моки
|
||
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
|
||
mock_cart_service.save_user_cart = AsyncMock(return_value=True)
|
||
mock_keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[[InlineKeyboardButton(text="Пополнить", callback_data="topup")]]
|
||
)
|
||
mock_keyboard_func.return_value = mock_keyboard
|
||
|
||
# Подготовим тексты
|
||
mock_texts = AsyncMock()
|
||
mock_texts.format_price = lambda x: f"{x/100:.0f} ₽"
|
||
mock_texts.t = lambda key, default: default
|
||
mock_get_texts.return_value = mock_texts
|
||
|
||
# Баланс пользователя меньше стоимости подписки
|
||
mock_user.balance_kopeks = 10000
|
||
|
||
# Вызываем функцию
|
||
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
# Проверяем, что FSM не был изменен (данные не установлены)
|
||
mock_state.set_data.assert_not_called()
|
||
|
||
# Проверяем, что сообщение было отредактировано с сообщением о недостатке средств
|
||
mock_callback_query.message.edit_text.assert_called_once()
|
||
|
||
# В этой функции в сценарии недостатка средств вызова callback.answer() не происходит
|
||
# (ответ отправляется через return до вызова callback.answer())
|
||
mock_callback_query.answer.assert_not_called()
|
||
|
||
async def test_clear_saved_cart(mock_callback_query, mock_state, mock_user, mock_db):
|
||
"""Тест очистки сохраненной корзины"""
|
||
# Мокаем все зависимости
|
||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.menu.show_main_menu') as mock_show_main_menu:
|
||
|
||
mock_cart_service.delete_user_cart = AsyncMock(return_value=True)
|
||
mock_show_main_menu.return_value = AsyncMock()
|
||
|
||
# Вызываем функцию
|
||
await clear_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
# Проверяем, что корзина удалена из сервиса
|
||
mock_cart_service.delete_user_cart.assert_called_once_with(mock_user.id)
|
||
|
||
# Проверяем, что FSM очищен
|
||
mock_state.clear.assert_called_once()
|
||
|
||
# Проверяем, что вызван answer
|
||
mock_callback_query.answer.assert_called_once()
|
||
|
||
async def test_handle_subscription_cancel_clears_saved_cart(mock_callback_query, mock_state, mock_user, mock_db):
|
||
"""Отмена покупки должна очищать сохраненную корзину"""
|
||
mock_clear_draft = AsyncMock()
|
||
mock_show_main_menu = AsyncMock()
|
||
|
||
with patch('app.handlers.subscription.autopay.user_cart_service') as mock_cart_service, \
|
||
patch('app.handlers.subscription.autopay.clear_subscription_checkout_draft', new=mock_clear_draft), \
|
||
patch('app.localization.texts.get_texts', return_value=MagicMock()) as _, \
|
||
patch('app.handlers.menu.show_main_menu', new=mock_show_main_menu):
|
||
|
||
mock_cart_service.delete_user_cart = AsyncMock(return_value=True)
|
||
|
||
await handle_subscription_cancel(mock_callback_query, mock_state, mock_user, mock_db)
|
||
|
||
mock_state.clear.assert_called_once()
|
||
mock_clear_draft.assert_awaited_once_with(mock_user.id)
|
||
mock_cart_service.delete_user_cart.assert_awaited_once_with(mock_user.id)
|
||
mock_show_main_menu.assert_awaited_once_with(mock_callback_query, mock_user, mock_db)
|
||
mock_callback_query.answer.assert_called_once_with("❌ Покупка отменена")
|
||
|