mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
1) Отображение скидки на кнопках (красивое!)
2) У промогрупп появится приоритет 3) У пользователя может быть несколько промогрупп, но влиять будет только с наивысшим приоритетом 4) К промокодам можно будет добавить промогруппу. Все активировавшие промокод получат её 5) При выводе пользователей с промогруппой будет также выводиться ссылка на каждого. Можно будет отследить сливы промокодов "для своих". Я в целом это добавлю во все места, где пользователь выводится в админке 6) Исправить баг исчезновения триалки при пополнении 7) Исправить падающие тесты и добавить новых 8) Трафик: 0 ГБ в тестовой подписке исправить на Трафик: Безлимит
This commit is contained in:
@@ -6,9 +6,14 @@ import os
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add project root to Python path for imports
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# Подменяем параметры подключения к БД, чтобы SQLAlchemy не требовал aiosqlite.
|
||||
os.environ.setdefault("DATABASE_MODE", "postgresql")
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://user:pass@localhost/test_db")
|
||||
|
||||
0
tests/crud/__init__.py
Normal file
0
tests/crud/__init__.py
Normal file
142
tests/crud/test_promocode_crud.py
Normal file
142
tests/crud/test_promocode_crud.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Tests for Promocode CRUD operations - focus on promo_group_id integration
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.database.crud.promocode import (
|
||||
create_promocode,
|
||||
get_promocode_by_code,
|
||||
get_promocodes_list,
|
||||
)
|
||||
from app.database.models import PromoCodeType, PromoCode
|
||||
|
||||
# Import fixtures
|
||||
from tests.fixtures.promocode_fixtures import (
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
)
|
||||
|
||||
|
||||
async def test_create_promocode_with_promo_group_id(
|
||||
sample_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test creating a promocode with promo_group_id
|
||||
|
||||
Scenario:
|
||||
- Create PROMO_GROUP type promocode
|
||||
- promo_group_id should be saved
|
||||
- Database operations should be called correctly
|
||||
"""
|
||||
# Execute
|
||||
promocode = await create_promocode(
|
||||
db=mock_db_session,
|
||||
code="TESTGROUP",
|
||||
type=PromoCodeType.PROMO_GROUP,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=0,
|
||||
max_uses=100,
|
||||
valid_until=None,
|
||||
created_by=1,
|
||||
promo_group_id=sample_promo_group.id
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert promocode.code == "TESTGROUP"
|
||||
assert promocode.type == PromoCodeType.PROMO_GROUP.value
|
||||
assert promocode.promo_group_id == sample_promo_group.id
|
||||
|
||||
# Verify database operations
|
||||
mock_db_session.add.assert_called_once()
|
||||
mock_db_session.commit.assert_awaited_once()
|
||||
mock_db_session.refresh.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_create_promocode_without_promo_group_id(mock_db_session):
|
||||
"""
|
||||
Test creating a promocode without promo_group_id (other types)
|
||||
|
||||
Scenario:
|
||||
- Create BALANCE type promocode
|
||||
- promo_group_id should be None
|
||||
"""
|
||||
# Execute
|
||||
promocode = await create_promocode(
|
||||
db=mock_db_session,
|
||||
code="BALANCE100",
|
||||
type=PromoCodeType.BALANCE,
|
||||
balance_bonus_kopeks=10000,
|
||||
subscription_days=0,
|
||||
max_uses=50,
|
||||
valid_until=None,
|
||||
created_by=1,
|
||||
promo_group_id=None
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert promocode.code == "BALANCE100"
|
||||
assert promocode.type == PromoCodeType.BALANCE.value
|
||||
assert promocode.promo_group_id is None
|
||||
|
||||
|
||||
async def test_get_promocode_by_code_loads_promo_group(
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test that get_promocode_by_code loads promo_group relationship
|
||||
|
||||
Scenario:
|
||||
- Query promocode by code
|
||||
- Verify selectinload was used for promo_group
|
||||
- Verify promo_group data is accessible
|
||||
"""
|
||||
# Setup mock result
|
||||
mock_result = AsyncMock()
|
||||
mock_result.scalar_one_or_none = lambda: sample_promocode_promo_group
|
||||
mock_db_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
# Execute
|
||||
promocode = await get_promocode_by_code(mock_db_session, "VIPGROUP")
|
||||
|
||||
# Assertions
|
||||
assert promocode is not None
|
||||
assert promocode.code == "VIPGROUP"
|
||||
assert promocode.promo_group is not None
|
||||
assert promocode.promo_group.name == "Test VIP Group"
|
||||
|
||||
# Verify execute was called (query was executed)
|
||||
mock_db_session.execute.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_get_promocodes_list_loads_promo_groups(
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test that get_promocodes_list loads promo_group relationships
|
||||
|
||||
Scenario:
|
||||
- Query list of promocodes
|
||||
- Verify selectinload was used for promo_group
|
||||
- Verify all promocodes have accessible promo_group data
|
||||
"""
|
||||
# Setup mock result
|
||||
mock_result = AsyncMock()
|
||||
mock_result.scalars = MagicMock(return_value=MagicMock(all=MagicMock(return_value=[sample_promocode_promo_group])))
|
||||
mock_db_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
# Execute
|
||||
promocodes = await get_promocodes_list(mock_db_session, offset=0, limit=10)
|
||||
|
||||
# Assertions
|
||||
assert len(promocodes) == 1
|
||||
assert promocodes[0].promo_group is not None
|
||||
assert promocodes[0].promo_group.name == "Test VIP Group"
|
||||
|
||||
# Verify execute was called
|
||||
mock_db_session.execute.assert_awaited_once()
|
||||
1
tests/fixtures/__init__.py
vendored
Normal file
1
tests/fixtures/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Test fixtures package"""
|
||||
206
tests/fixtures/promocode_fixtures.py
vendored
Normal file
206
tests/fixtures/promocode_fixtures.py
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Fixtures for promocode and promo group testing
|
||||
"""
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.database.models import PromoCodeType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_promo_group():
|
||||
"""Sample PromoGroup object for testing"""
|
||||
return SimpleNamespace(
|
||||
id=1,
|
||||
name="Test VIP Group",
|
||||
priority=50,
|
||||
server_discount_percent=20,
|
||||
traffic_discount_percent=15,
|
||||
device_discount_percent=10,
|
||||
period_discounts={30: 10, 60: 15, 90: 20},
|
||||
is_default=False,
|
||||
auto_assign_total_spent_kopeks=None,
|
||||
auto_assign_enabled=False,
|
||||
addon_discount_enabled=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user():
|
||||
"""Sample User object for testing"""
|
||||
return SimpleNamespace(
|
||||
id=1,
|
||||
telegram_id=123456789,
|
||||
username="testuser",
|
||||
full_name="Test User",
|
||||
balance_kopeks=0,
|
||||
language="ru",
|
||||
has_had_paid_subscription=False,
|
||||
total_spent_kopeks=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_promocode_balance():
|
||||
"""Balance type promocode"""
|
||||
return SimpleNamespace(
|
||||
id=1,
|
||||
code="BALANCE100",
|
||||
type=PromoCodeType.BALANCE.value,
|
||||
balance_bonus_kopeks=10000, # 100 rubles
|
||||
subscription_days=0,
|
||||
max_uses=100,
|
||||
current_uses=10,
|
||||
is_active=True,
|
||||
promo_group_id=None,
|
||||
promo_group=None,
|
||||
valid_until=None,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
created_by=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_promocode_subscription():
|
||||
"""Subscription days type promocode"""
|
||||
return SimpleNamespace(
|
||||
id=2,
|
||||
code="SUB30",
|
||||
type=PromoCodeType.SUBSCRIPTION_DAYS.value,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=30,
|
||||
max_uses=50,
|
||||
current_uses=5,
|
||||
is_active=True,
|
||||
promo_group_id=None,
|
||||
promo_group=None,
|
||||
valid_until=datetime.utcnow() + timedelta(days=60),
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
created_by=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_promocode_promo_group(sample_promo_group):
|
||||
"""Promo group type promocode"""
|
||||
return SimpleNamespace(
|
||||
id=3,
|
||||
code="VIPGROUP",
|
||||
type=PromoCodeType.PROMO_GROUP.value,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=0,
|
||||
max_uses=100,
|
||||
current_uses=20,
|
||||
is_active=True,
|
||||
promo_group_id=sample_promo_group.id,
|
||||
promo_group=sample_promo_group,
|
||||
valid_until=None,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
created_by=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_promocode_invalid():
|
||||
"""Invalid/expired promocode"""
|
||||
return SimpleNamespace(
|
||||
id=4,
|
||||
code="EXPIRED",
|
||||
type=PromoCodeType.BALANCE.value,
|
||||
balance_bonus_kopeks=5000,
|
||||
subscription_days=0,
|
||||
max_uses=10,
|
||||
current_uses=10, # Used up
|
||||
is_active=False,
|
||||
promo_group_id=None,
|
||||
promo_group=None,
|
||||
valid_until=datetime.utcnow() - timedelta(days=1), # Expired
|
||||
created_at=datetime.utcnow() - timedelta(days=30),
|
||||
updated_at=datetime.utcnow(),
|
||||
created_by=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Mock AsyncSession"""
|
||||
db = AsyncMock()
|
||||
db.commit = AsyncMock()
|
||||
db.rollback = AsyncMock()
|
||||
db.refresh = AsyncMock()
|
||||
db.get = AsyncMock()
|
||||
db.execute = AsyncMock()
|
||||
db.add = AsyncMock()
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_has_user_promo_group():
|
||||
"""Mock has_user_promo_group function"""
|
||||
return AsyncMock(return_value=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_add_user_to_promo_group():
|
||||
"""Mock add_user_to_promo_group function"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_promo_group_by_id(sample_promo_group):
|
||||
"""Mock get_promo_group_by_id function"""
|
||||
return AsyncMock(return_value=sample_promo_group)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_user_by_id(sample_user):
|
||||
"""Mock get_user_by_id function"""
|
||||
return AsyncMock(return_value=sample_user)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_promocode_by_code():
|
||||
"""Mock get_promocode_by_code function"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_check_user_promocode_usage():
|
||||
"""Mock check_user_promocode_usage function"""
|
||||
return AsyncMock(return_value=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_create_promocode_use():
|
||||
"""Mock create_promocode_use function"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_remnawave_service():
|
||||
"""Mock RemnaWaveService"""
|
||||
service = AsyncMock()
|
||||
service.create_remnawave_user = AsyncMock()
|
||||
service.update_remnawave_user = AsyncMock()
|
||||
return service
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_subscription_service():
|
||||
"""Mock SubscriptionService"""
|
||||
service = AsyncMock()
|
||||
service.create_remnawave_user = AsyncMock()
|
||||
service.update_remnawave_user = AsyncMock()
|
||||
return service
|
||||
|
||||
|
||||
# Helper function to create a valid promocode property mock
|
||||
def make_promocode_valid(promocode):
|
||||
"""Helper to make promocode appear valid (is_valid property)"""
|
||||
promocode.is_valid = True
|
||||
return promocode
|
||||
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
338
tests/integration/test_promocode_promo_group_flow.py
Normal file
338
tests/integration/test_promocode_promo_group_flow.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Integration tests for promo code with promo group full workflow
|
||||
|
||||
These tests validate the complete flow from creating a promo group,
|
||||
creating a promocode, to activating it and verifying the user receives
|
||||
the promo group assignment.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.promocode_service import PromoCodeService
|
||||
from app.database.models import PromoCodeType
|
||||
|
||||
# Import fixtures
|
||||
from tests.fixtures.promocode_fixtures import (
|
||||
sample_promo_group,
|
||||
sample_user,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
)
|
||||
|
||||
|
||||
async def test_promo_group_promocode_full_workflow(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Integration test: Full workflow of promo group promocode
|
||||
|
||||
Flow:
|
||||
1. Promo group exists (VIP Group, priority 50)
|
||||
2. Admin creates PROMO_GROUP type promocode
|
||||
3. User activates promocode
|
||||
4. User is added to promo group
|
||||
5. Usage is recorded
|
||||
6. Counter is incremented
|
||||
|
||||
This test validates the entire integration between:
|
||||
- Promocode CRUD
|
||||
- Promo group CRUD
|
||||
- User promo group CRUD
|
||||
- Promocode service
|
||||
"""
|
||||
# Setup: Create a PROMO_GROUP promocode
|
||||
promocode = SimpleNamespace(
|
||||
id=1,
|
||||
code="INTEGRATIONTEST",
|
||||
type=PromoCodeType.PROMO_GROUP.value,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=0,
|
||||
max_uses=100,
|
||||
current_uses=0,
|
||||
is_active=True,
|
||||
is_valid=True,
|
||||
promo_group_id=sample_promo_group.id,
|
||||
promo_group=sample_promo_group,
|
||||
valid_until=None
|
||||
)
|
||||
|
||||
# Mock all CRUD operations
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=promocode)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute: User activates promocode
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"INTEGRATIONTEST"
|
||||
)
|
||||
|
||||
# Verify: Activation successful
|
||||
assert result["success"] is True
|
||||
assert "Test VIP Group" in result["description"]
|
||||
|
||||
# Verify: All steps were executed in correct order
|
||||
get_user_mock.assert_awaited_once_with(mock_db_session, sample_user.id)
|
||||
get_promocode_mock.assert_awaited_once_with(mock_db_session, "INTEGRATIONTEST")
|
||||
check_usage_mock.assert_awaited_once_with(mock_db_session, sample_user.id, promocode.id)
|
||||
|
||||
# Verify: Promo group assignment flow
|
||||
get_promo_group_mock.assert_awaited_once_with(mock_db_session, sample_promo_group.id)
|
||||
has_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
sample_promo_group.id
|
||||
)
|
||||
add_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
sample_promo_group.id,
|
||||
assigned_by="promocode"
|
||||
)
|
||||
|
||||
# Verify: Usage recorded
|
||||
create_usage_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
promocode.id,
|
||||
sample_user.id
|
||||
)
|
||||
|
||||
# Verify: Counter incremented
|
||||
assert promocode.current_uses == 1
|
||||
|
||||
# Verify: Database committed
|
||||
mock_db_session.commit.assert_awaited()
|
||||
|
||||
|
||||
async def test_duplicate_promo_group_assignment_edge_case(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Edge case: User already has promo group from previous promocode
|
||||
|
||||
Scenario:
|
||||
1. User previously activated a promo group promocode
|
||||
2. User already has the VIP Group
|
||||
3. User activates another promocode for same group
|
||||
4. System should not duplicate the assignment
|
||||
5. Activation should still succeed
|
||||
"""
|
||||
promocode = SimpleNamespace(
|
||||
id=2,
|
||||
code="DUPLICATE",
|
||||
type=PromoCodeType.PROMO_GROUP.value,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=0,
|
||||
max_uses=100,
|
||||
current_uses=5,
|
||||
is_active=True,
|
||||
is_valid=True,
|
||||
promo_group_id=sample_promo_group.id,
|
||||
promo_group=sample_promo_group,
|
||||
valid_until=None
|
||||
)
|
||||
|
||||
# Mock CRUD operations
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=promocode)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
# User ALREADY HAS this promo group
|
||||
has_promo_group_mock = AsyncMock(return_value=True)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"DUPLICATE"
|
||||
)
|
||||
|
||||
# Verify: Activation still successful
|
||||
assert result["success"] is True
|
||||
|
||||
# Verify: add_user_to_promo_group was NOT called (no duplicate)
|
||||
add_promo_group_mock.assert_not_awaited()
|
||||
|
||||
# Verify: Usage was still recorded
|
||||
create_usage_mock.assert_awaited_once()
|
||||
|
||||
# Verify: Counter still incremented
|
||||
assert promocode.current_uses == 6
|
||||
|
||||
|
||||
async def test_missing_promo_group_graceful_failure(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Edge case: Promocode references deleted/non-existent promo group
|
||||
|
||||
Scenario:
|
||||
1. Promocode was created with promo_group_id=999
|
||||
2. Promo group was later deleted
|
||||
3. User activates promocode
|
||||
4. System should handle gracefully (log warning, continue)
|
||||
5. Promocode effects should still apply
|
||||
6. No promo group is assigned (can't assign non-existent group)
|
||||
"""
|
||||
# Promocode with non-existent promo_group_id
|
||||
promocode = SimpleNamespace(
|
||||
id=3,
|
||||
code="ORPHANED",
|
||||
type=PromoCodeType.PROMO_GROUP.value,
|
||||
balance_bonus_kopeks=0,
|
||||
subscription_days=0,
|
||||
max_uses=10,
|
||||
current_uses=0,
|
||||
is_active=True,
|
||||
is_valid=True,
|
||||
promo_group_id=999, # Non-existent
|
||||
promo_group=None,
|
||||
valid_until=None
|
||||
)
|
||||
|
||||
# Mock CRUD operations
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=promocode)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
# Promo group NOT FOUND
|
||||
get_promo_group_mock = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"ORPHANED"
|
||||
)
|
||||
|
||||
# Verify: Activation STILL successful (graceful degradation)
|
||||
assert result["success"] is True
|
||||
|
||||
# Verify: Attempted to fetch promo group
|
||||
get_promo_group_mock.assert_awaited_once_with(mock_db_session, 999)
|
||||
|
||||
# Verify: add_user_to_promo_group was NOT called (group doesn't exist)
|
||||
add_promo_group_mock.assert_not_awaited()
|
||||
|
||||
# Verify: Usage was still recorded (promocode still works)
|
||||
create_usage_mock.assert_awaited_once()
|
||||
|
||||
# Verify: Counter still incremented
|
||||
assert promocode.current_uses == 1
|
||||
584
tests/services/test_promocode_service.py
Normal file
584
tests/services/test_promocode_service.py
Normal file
@@ -0,0 +1,584 @@
|
||||
"""
|
||||
Tests for PromoCodeService - focus on promo group integration
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.promocode_service import PromoCodeService
|
||||
from app.database.models import PromoCodeType
|
||||
|
||||
# Import fixtures
|
||||
from tests.fixtures.promocode_fixtures import (
|
||||
sample_promo_group,
|
||||
sample_user,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
mock_has_user_promo_group,
|
||||
mock_add_user_to_promo_group,
|
||||
mock_get_promo_group_by_id,
|
||||
mock_get_user_by_id,
|
||||
mock_get_promocode_by_code,
|
||||
mock_check_user_promocode_usage,
|
||||
mock_create_promocode_use,
|
||||
)
|
||||
|
||||
|
||||
async def test_activate_promo_group_promocode_success(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test successful activation of PROMO_GROUP type promocode
|
||||
|
||||
Scenario:
|
||||
- User activates valid promo group promocode
|
||||
- User doesn't have this promo group yet
|
||||
- User is successfully added to promo group
|
||||
- Result includes promo group name
|
||||
"""
|
||||
# Make promocode valid
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result["success"] is True
|
||||
assert "Test VIP Group" in result["description"]
|
||||
assert result["promocode"]["promo_group_id"] == sample_promo_group.id
|
||||
|
||||
# Verify promo group was fetched
|
||||
get_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_promo_group.id
|
||||
)
|
||||
|
||||
# Verify user promo group check
|
||||
has_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
sample_promo_group.id
|
||||
)
|
||||
|
||||
# Verify promo group assignment
|
||||
add_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
sample_promo_group.id,
|
||||
assigned_by="promocode"
|
||||
)
|
||||
|
||||
# Verify usage recorded
|
||||
create_usage_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_promocode_promo_group.id,
|
||||
sample_user.id
|
||||
)
|
||||
|
||||
# Verify counter incremented
|
||||
assert sample_promocode_promo_group.current_uses == 21
|
||||
mock_db_session.commit.assert_awaited()
|
||||
|
||||
|
||||
async def test_activate_promo_group_user_already_has_group(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test activation when user already has the promo group
|
||||
|
||||
Scenario:
|
||||
- User activates promo group promocode
|
||||
- User already has this promo group
|
||||
- add_user_to_promo_group should NOT be called
|
||||
- Activation still succeeds
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
# User ALREADY HAS the promo group
|
||||
has_promo_group_mock = AsyncMock(return_value=True)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result["success"] is True
|
||||
|
||||
# Verify promo group assignment was NOT called
|
||||
add_promo_group_mock.assert_not_awaited()
|
||||
|
||||
# But usage was still recorded
|
||||
create_usage_mock.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_activate_promo_group_group_not_found(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test activation when promo group doesn't exist (deleted/invalid)
|
||||
|
||||
Scenario:
|
||||
- Promocode references non-existent promo_group_id
|
||||
- get_promo_group_by_id returns None
|
||||
- Warning is logged but activation doesn't fail
|
||||
- Promocode effects still apply (graceful degradation)
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
# Promo group NOT FOUND
|
||||
get_promo_group_mock = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result["success"] is True # Still succeeds!
|
||||
|
||||
# Verify promo group was attempted to fetch
|
||||
get_promo_group_mock.assert_awaited_once()
|
||||
|
||||
# Verify promo group assignment was NOT called (because group not found)
|
||||
add_promo_group_mock.assert_not_awaited()
|
||||
|
||||
# But usage was still recorded
|
||||
create_usage_mock.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_activate_promo_group_assignment_error(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test activation when promo group assignment fails
|
||||
|
||||
Scenario:
|
||||
- add_user_to_promo_group raises exception
|
||||
- Error is logged but activation doesn't fail
|
||||
- Promocode usage is still recorded (graceful degradation)
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
# add_user_to_promo_group RAISES EXCEPTION
|
||||
add_promo_group_mock = AsyncMock(side_effect=Exception("Database error"))
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result["success"] is True # Still succeeds!
|
||||
|
||||
# Verify promo group assignment was attempted
|
||||
add_promo_group_mock.assert_awaited_once()
|
||||
|
||||
# But usage was still recorded
|
||||
create_usage_mock.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_activate_promo_group_assigned_by_value(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test that assigned_by parameter is correctly set to 'promocode'
|
||||
|
||||
Scenario:
|
||||
- Verify add_user_to_promo_group is called with assigned_by="promocode"
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Verify assigned_by="promocode"
|
||||
add_promo_group_mock.assert_awaited_once_with(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
sample_promo_group.id,
|
||||
assigned_by="promocode" # Critical assertion
|
||||
)
|
||||
|
||||
|
||||
async def test_activate_promo_group_description_includes_group_name(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test that result description includes promo group name
|
||||
|
||||
Scenario:
|
||||
- When promo group is assigned, description should include group name
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Verify description includes promo group name
|
||||
assert "Назначена промогруппа: Test VIP Group" in result["description"]
|
||||
|
||||
|
||||
async def test_promocode_data_includes_promo_group_id(
|
||||
monkeypatch,
|
||||
sample_user,
|
||||
sample_promo_group,
|
||||
sample_promocode_promo_group,
|
||||
mock_db_session,
|
||||
):
|
||||
"""
|
||||
Test that returned promocode data includes promo_group_id
|
||||
|
||||
Scenario:
|
||||
- Verify result["promocode"]["promo_group_id"] is present
|
||||
"""
|
||||
sample_promocode_promo_group.is_valid = True
|
||||
|
||||
# Mock CRUD functions
|
||||
get_user_mock = AsyncMock(return_value=sample_user)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_user_by_id',
|
||||
get_user_mock
|
||||
)
|
||||
|
||||
get_promocode_mock = AsyncMock(return_value=sample_promocode_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promocode_by_code',
|
||||
get_promocode_mock
|
||||
)
|
||||
|
||||
check_usage_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.check_user_promocode_usage',
|
||||
check_usage_mock
|
||||
)
|
||||
|
||||
get_promo_group_mock = AsyncMock(return_value=sample_promo_group)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.get_promo_group_by_id',
|
||||
get_promo_group_mock
|
||||
)
|
||||
|
||||
has_promo_group_mock = AsyncMock(return_value=False)
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.has_user_promo_group',
|
||||
has_promo_group_mock
|
||||
)
|
||||
|
||||
add_promo_group_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.add_user_to_promo_group',
|
||||
add_promo_group_mock
|
||||
)
|
||||
|
||||
create_usage_mock = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
'app.services.promocode_service.create_promocode_use',
|
||||
create_usage_mock
|
||||
)
|
||||
|
||||
# Execute
|
||||
service = PromoCodeService()
|
||||
result = await service.activate_promocode(
|
||||
mock_db_session,
|
||||
sample_user.id,
|
||||
"VIPGROUP"
|
||||
)
|
||||
|
||||
# Verify promocode data structure
|
||||
assert "promocode" in result
|
||||
assert "promo_group_id" in result["promocode"]
|
||||
assert result["promocode"]["promo_group_id"] == sample_promo_group.id
|
||||
@@ -12,7 +12,6 @@ if str(ROOT_DIR) not in sys.path:
|
||||
from app.services import referral_service # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commission_accrues_before_minimum_first_topup(monkeypatch):
|
||||
user = SimpleNamespace(
|
||||
id=1,
|
||||
|
||||
@@ -66,7 +66,6 @@ def test_deduplicate_ignores_records_without_expire_date():
|
||||
assert deduplicated[telegram_id] is valid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_user_handles_unique_violation(monkeypatch):
|
||||
service = _create_service()
|
||||
db = AsyncMock()
|
||||
@@ -97,7 +96,6 @@ async def test_get_or_create_user_handles_unique_violation(monkeypatch):
|
||||
rollback_mock.assert_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_user_creates_new(monkeypatch):
|
||||
service = _create_service()
|
||||
db = AsyncMock()
|
||||
|
||||
@@ -25,7 +25,6 @@ class DummyTexts:
|
||||
return f"{value / 100:.0f} ₽"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
|
||||
monkeypatch.setattr(settings, "AUTO_PURCHASE_AFTER_TOPUP_ENABLED", True)
|
||||
|
||||
@@ -185,7 +184,6 @@ async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
|
||||
admin_service_mock.send_subscription_purchase_notification.assert_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_purchase_saved_cart_after_topup_extension(monkeypatch):
|
||||
monkeypatch.setattr(settings, "AUTO_PURCHASE_AFTER_TOPUP_ENABLED", True)
|
||||
|
||||
@@ -298,3 +296,379 @@ async def test_auto_purchase_saved_cart_after_topup_extension(monkeypatch):
|
||||
bot.send_message.assert_awaited()
|
||||
service_mock.update_remnawave_user.assert_awaited()
|
||||
create_transaction_mock.assert_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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 был вызван
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
|
||||
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 "",
|
||||
)
|
||||
|
||||
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 БЫЛ ВЫЗВАН!
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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}"
|
||||
|
||||
@@ -12,7 +12,6 @@ from app.config import settings
|
||||
from app.services.system_settings_service import bot_configuration_service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_env_override_prevents_set_value(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
@@ -45,7 +44,6 @@ async def test_env_override_prevents_set_value(monkeypatch):
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_env_override_prevents_reset_value(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
@@ -77,7 +75,6 @@ async def test_env_override_prevents_reset_value(monkeypatch):
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_skips_db_value_for_env_override(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
@@ -130,7 +127,6 @@ async def test_initialize_skips_db_value_for_env_override(monkeypatch):
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_value_applies_without_env_override(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ def mock_state():
|
||||
state.clear = AsyncMock()
|
||||
return state
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_cart_and_redirect_to_topup(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Тест сохранения корзины и перенаправления к пополнению"""
|
||||
# Мокаем все зависимости
|
||||
@@ -102,7 +101,6 @@ async def test_save_cart_and_redirect_to_topup(mock_callback_query, mock_state,
|
||||
# mock_callback_query.answer не должен быть вызван
|
||||
mock_callback_query.answer.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_success(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Тест возврата к сохраненной корзине с достаточным балансом"""
|
||||
# Подготовим данные корзины
|
||||
@@ -153,7 +151,6 @@ async def test_return_to_saved_cart_success(mock_callback_query, mock_state, moc
|
||||
mock_callback_query.answer.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_skips_edit_when_message_matches(
|
||||
mock_callback_query,
|
||||
mock_state,
|
||||
@@ -224,7 +221,6 @@ async def test_return_to_saved_cart_skips_edit_when_message_matches(
|
||||
mock_cart_service.save_user_cart.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_normalizes_devices_when_disabled(
|
||||
mock_callback_query,
|
||||
mock_state,
|
||||
@@ -299,7 +295,6 @@ async def test_return_to_saved_cart_normalizes_devices_when_disabled(
|
||||
|
||||
mock_callback_query.answer.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_insufficient_funds(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Тест возврата к сохраненной корзине с недостаточным балансом"""
|
||||
# Подготовим данные корзины
|
||||
@@ -347,7 +342,6 @@ async def test_return_to_saved_cart_insufficient_funds(mock_callback_query, mock
|
||||
# (ответ отправляется через return до вызова callback.answer())
|
||||
mock_callback_query.answer.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_saved_cart(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Тест очистки сохраненной корзины"""
|
||||
# Мокаем все зависимости
|
||||
@@ -369,7 +363,6 @@ async def test_clear_saved_cart(mock_callback_query, mock_state, mock_user, mock
|
||||
# Проверяем, что вызван answer
|
||||
mock_callback_query.answer.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_subscription_cancel_clears_saved_cart(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Отмена покупки должна очищать сохраненную корзину"""
|
||||
mock_clear_draft = AsyncMock()
|
||||
|
||||
@@ -34,7 +34,6 @@ def user_cart_service(mock_redis):
|
||||
service.redis_client = mock_redis
|
||||
return service
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_user_cart(user_cart_service, mock_redis):
|
||||
"""Тест сохранения корзины пользователя"""
|
||||
user_id = 12345
|
||||
@@ -52,7 +51,6 @@ async def test_save_user_cart(user_cart_service, mock_redis):
|
||||
assert f"user_cart:{user_id}" in mock_redis.storage
|
||||
assert cart_data == eval(mock_redis.storage[f"user_cart:{user_id}"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_cart(user_cart_service, mock_redis):
|
||||
"""Тест получения корзины пользователя"""
|
||||
user_id = 12345
|
||||
@@ -72,7 +70,6 @@ async def test_get_user_cart(user_cart_service, mock_redis):
|
||||
|
||||
assert result == cart_data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_cart_not_found(user_cart_service):
|
||||
"""Тест получения несуществующей корзины пользователя"""
|
||||
user_id = 99999
|
||||
@@ -81,7 +78,6 @@ async def test_get_user_cart_not_found(user_cart_service):
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_cart(user_cart_service, mock_redis):
|
||||
"""Тест удаления корзины пользователя"""
|
||||
user_id = 12345
|
||||
@@ -103,7 +99,6 @@ async def test_delete_user_cart(user_cart_service, mock_redis):
|
||||
assert result is True
|
||||
assert f"user_cart:{user_id}" not in mock_redis.storage
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_cart_not_found(user_cart_service):
|
||||
"""Тест удаления несуществующей корзины пользователя"""
|
||||
user_id = 99999
|
||||
@@ -113,7 +108,6 @@ async def test_delete_user_cart_not_found(user_cart_service):
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_user_cart(user_cart_service, mock_redis):
|
||||
"""Тест проверки наличия корзины пользователя"""
|
||||
user_id = 12345
|
||||
@@ -136,7 +130,6 @@ async def test_has_user_cart(user_cart_service, mock_redis):
|
||||
result = await user_cart_service.has_user_cart(user_id)
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_user_cart_not_found(user_cart_service):
|
||||
"""Тест проверки отсутствия корзины пользователя"""
|
||||
user_id = 99999
|
||||
|
||||
405
tests/utils/test_pricing_utils.py
Normal file
405
tests/utils/test_pricing_utils.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
Тесты для утилит ценообразования и форматирования цен.
|
||||
|
||||
Этот модуль тестирует функции из app/utils/pricing_utils.py и app/localization/texts.py,
|
||||
особенно функции отображения цен со скидками на кнопках подписки.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.utils.pricing_utils import format_period_option_label
|
||||
from app.localization.texts import _build_dynamic_values
|
||||
|
||||
|
||||
class TestFormatPeriodOptionLabel:
|
||||
"""Тесты для функции format_period_option_label."""
|
||||
|
||||
def test_format_with_price_only_no_discount(self) -> None:
|
||||
"""Цена без скидки должна отображаться в простом формате."""
|
||||
result = format_period_option_label("📅 30 дней", 99000)
|
||||
assert result == "📅 30 дней - 990 ₽"
|
||||
|
||||
def test_format_with_discount_shows_strikethrough(self) -> None:
|
||||
"""Цена со скидкой должна показывать зачёркнутую оригинальную цену."""
|
||||
result = format_period_option_label(
|
||||
"📅 30 дней",
|
||||
price=69300,
|
||||
original_price=99000,
|
||||
discount_percent=30
|
||||
)
|
||||
assert result == "📅 30 дней - <s>990 ₽</s> 693 ₽ (-30%)"
|
||||
|
||||
def test_format_with_zero_price_returns_label_only(self) -> None:
|
||||
"""Нулевая цена должна возвращать только метку без цены."""
|
||||
result = format_period_option_label("📅 30 дней", 0)
|
||||
assert result == "📅 30 дней"
|
||||
|
||||
def test_format_with_negative_price_returns_label_only(self) -> None:
|
||||
"""Отрицательная цена должна возвращать только метку."""
|
||||
result = format_period_option_label("📅 30 дней", -1000)
|
||||
assert result == "📅 30 дней"
|
||||
|
||||
def test_format_with_zero_discount_percent_shows_simple_price(self) -> None:
|
||||
"""Нулевая скидка должна отображать простую цену без зачёркивания."""
|
||||
result = format_period_option_label(
|
||||
"📅 30 дней",
|
||||
price=99000,
|
||||
original_price=99000,
|
||||
discount_percent=0
|
||||
)
|
||||
assert result == "📅 30 дней - 990 ₽"
|
||||
|
||||
def test_format_with_original_price_equal_to_final_shows_simple(self) -> None:
|
||||
"""Если оригинальная цена равна финальной, показывать простой формат."""
|
||||
result = format_period_option_label(
|
||||
"📅 30 дней",
|
||||
price=99000,
|
||||
original_price=99000,
|
||||
discount_percent=10 # Указана скидка, но цены равны
|
||||
)
|
||||
assert result == "📅 30 дней - 990 ₽"
|
||||
|
||||
def test_format_with_original_price_less_than_final_shows_simple(self) -> None:
|
||||
"""Если оригинальная цена меньше финальной (некорректно), показывать простой формат."""
|
||||
result = format_period_option_label(
|
||||
"📅 30 дней",
|
||||
price=99000,
|
||||
original_price=50000,
|
||||
discount_percent=10
|
||||
)
|
||||
assert result == "📅 30 дней - 990 ₽"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"label,price,original,discount,expected",
|
||||
[
|
||||
# Базовые случаи
|
||||
("📅 14 дней", 50000, 0, 0, "📅 14 дней - 500 ₽"),
|
||||
("📅 30 дней", 99000, 0, 0, "📅 30 дней - 990 ₽"),
|
||||
("📅 360 дней", 899000, 0, 0, "📅 360 дней - 8990 ₽"),
|
||||
|
||||
# Со скидками
|
||||
("📅 30 дней", 69300, 99000, 30, "📅 30 дней - <s>990 ₽</s> 693 ₽ (-30%)"),
|
||||
("📅 90 дней", 188300, 269000, 30, "📅 90 дней - <s>2690 ₽</s> 1883 ₽ (-30%)"),
|
||||
("📅 360 дней", 629300, 899000, 30, "📅 360 дней - <s>8990 ₽</s> 6293 ₽ (-30%)"),
|
||||
|
||||
# Разные проценты скидок
|
||||
("📅 30 дней", 89100, 99000, 10, "📅 30 дней - <s>990 ₽</s> 891 ₽ (-10%)"),
|
||||
("📅 30 дней", 49500, 99000, 50, "📅 30 дней - <s>990 ₽</s> 495 ₽ (-50%)"),
|
||||
|
||||
# Цены с копейками
|
||||
("📅 7 дней", 12345, 0, 0, "📅 7 дней - 123.45 ₽"),
|
||||
("📅 7 дней", 12350, 0, 0, "📅 7 дней - 123.5 ₽"),
|
||||
],
|
||||
)
|
||||
def test_format_various_scenarios(
|
||||
self,
|
||||
label: str,
|
||||
price: int,
|
||||
original: int,
|
||||
discount: int,
|
||||
expected: str
|
||||
) -> None:
|
||||
"""Различные сценарии форматирования должны работать корректно."""
|
||||
result = format_period_option_label(label, price, original, discount)
|
||||
assert result == expected
|
||||
|
||||
def test_format_with_100_percent_discount(self) -> None:
|
||||
"""100% скидка должна корректно отображаться."""
|
||||
result = format_period_option_label(
|
||||
"📅 30 дней",
|
||||
price=0,
|
||||
original_price=99000,
|
||||
discount_percent=100
|
||||
)
|
||||
# Цена 0, поэтому возвращается только label
|
||||
assert result == "📅 30 дней"
|
||||
|
||||
def test_format_preserves_label_emojis(self) -> None:
|
||||
"""Эмодзи в метке должны сохраняться."""
|
||||
result = format_period_option_label("🔥 📅 360 дней 🔥", 899000)
|
||||
assert result == "🔥 📅 360 дней 🔥 - 8990 ₽"
|
||||
|
||||
def test_format_with_large_prices(self) -> None:
|
||||
"""Большие цены должны корректно форматироваться."""
|
||||
result = format_period_option_label(
|
||||
"📅 720 дней",
|
||||
price=150000000, # 1,500,000 рублей
|
||||
original_price=200000000,
|
||||
discount_percent=25
|
||||
)
|
||||
assert result == "📅 720 дней - <s>2000000 ₽</s> 1500000 ₽ (-25%)"
|
||||
|
||||
def test_format_with_small_prices_kopeks(self) -> None:
|
||||
"""Маленькие цены с копейками должны корректно отображаться."""
|
||||
result = format_period_option_label(
|
||||
"📅 1 день",
|
||||
price=5050, # 50.50 рублей
|
||||
original_price=10000,
|
||||
discount_percent=50
|
||||
)
|
||||
assert result == "📅 1 день - <s>100 ₽</s> 50.5 ₽ (-50%)"
|
||||
|
||||
def test_format_without_optional_params_uses_defaults(self) -> None:
|
||||
"""Вызов без опциональных параметров должен использовать значения по умолчанию."""
|
||||
result = format_period_option_label("📅 30 дней", 99000)
|
||||
assert result == "📅 30 дней - 990 ₽"
|
||||
|
||||
|
||||
class TestBuildDynamicValues:
|
||||
"""Тесты для функции _build_dynamic_values из texts.py."""
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_russian_language_generates_period_keys(self, mock_settings: MagicMock) -> None:
|
||||
"""Русский язык должен генерировать все ключи периодов."""
|
||||
# Настройка моков
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 0
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Мок для traffic цен
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
assert "PERIOD_14_DAYS" in result
|
||||
assert "PERIOD_30_DAYS" in result
|
||||
assert "PERIOD_60_DAYS" in result
|
||||
assert "PERIOD_90_DAYS" in result
|
||||
assert "PERIOD_180_DAYS" in result
|
||||
assert "PERIOD_360_DAYS" in result
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_english_language_generates_period_keys(self, mock_settings: MagicMock) -> None:
|
||||
"""Английский язык должен генерировать все ключи периодов."""
|
||||
# Настройка моков
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 0
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Мок для traffic цен
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("en-US")
|
||||
|
||||
assert "PERIOD_14_DAYS" in result
|
||||
assert "PERIOD_30_DAYS" in result
|
||||
assert "PERIOD_360_DAYS" in result
|
||||
# Проверяем, что используется "days" а не "дней"
|
||||
assert "days" in result["PERIOD_30_DAYS"]
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
@patch('app.utils.pricing_utils.apply_percentage_discount')
|
||||
def test_period_with_discount_shows_strikethrough(
|
||||
self,
|
||||
mock_apply_discount: MagicMock,
|
||||
mock_settings: MagicMock
|
||||
) -> None:
|
||||
"""Период со скидкой должен показывать зачёркнутую цену."""
|
||||
# Настройка моков
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 30
|
||||
mock_apply_discount.return_value = (69300, 29700) # 30% скидка
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Остальные цены
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
# Проверяем, что есть зачёркивание и процент скидки
|
||||
assert "<s>990 ₽</s>" in result["PERIOD_30_DAYS"]
|
||||
assert "(-30%)" in result["PERIOD_30_DAYS"]
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_period_360_with_discount_has_fire_emojis(self, mock_settings: MagicMock) -> None:
|
||||
"""Период 360 дней со скидкой должен иметь огоньки 🔥."""
|
||||
# Настройка моков для 360 дней со скидкой
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
|
||||
def get_discount(period_days: int) -> int:
|
||||
return 30 if period_days == 360 else 0
|
||||
|
||||
mock_settings.get_base_promo_group_period_discount.side_effect = get_discount
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Остальные цены
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
# Проверяем наличие огоньков
|
||||
assert result["PERIOD_360_DAYS"].startswith("🔥")
|
||||
assert result["PERIOD_360_DAYS"].endswith("🔥")
|
||||
assert result["PERIOD_360_DAYS"].count("🔥") == 2
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_period_360_without_discount_no_fire_emojis(self, mock_settings: MagicMock) -> None:
|
||||
"""Период 360 дней без скидки НЕ должен иметь огоньки 🔥."""
|
||||
# Настройка моков для 360 дней БЕЗ скидки
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 0 # Нет скидки
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Остальные цены
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
# Проверяем отсутствие огоньков
|
||||
assert "🔥" not in result["PERIOD_360_DAYS"]
|
||||
# Но должна быть просто цена
|
||||
assert "8990 ₽" in result["PERIOD_360_DAYS"]
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_other_periods_never_have_fire_emojis(self, mock_settings: MagicMock) -> None:
|
||||
"""Другие периоды (не 360) никогда не должны иметь огоньки, даже со скидкой."""
|
||||
# Настройка моков - 30 дней со скидкой
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
|
||||
def get_discount(period_days: int) -> int:
|
||||
return 30 if period_days == 30 else 0
|
||||
|
||||
mock_settings.get_base_promo_group_period_discount.side_effect = get_discount
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
|
||||
# Остальные цены
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
# 30 дней со скидкой не должно иметь огоньков
|
||||
assert "🔥" not in result["PERIOD_30_DAYS"]
|
||||
# Но должна быть скидка
|
||||
assert "<s>" in result["PERIOD_30_DAYS"]
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_returns_empty_dict_for_unknown_language(self, mock_settings: MagicMock) -> None:
|
||||
"""Неизвестный язык должен возвращать пустой словарь."""
|
||||
result = _build_dynamic_values("fr-FR") # Французский не поддерживается
|
||||
assert result == {}
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_language_code_extraction_works(self, mock_settings: MagicMock) -> None:
|
||||
"""Должна корректно извлекаться языковая часть из locale."""
|
||||
# Настройка моков
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 0
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
# Тест с полным locale кодом
|
||||
result1 = _build_dynamic_values("ru-RU")
|
||||
result2 = _build_dynamic_values("ru")
|
||||
result3 = _build_dynamic_values("RU-ru")
|
||||
|
||||
# Все должны вернуть русские значения
|
||||
assert "дней" in result1["PERIOD_30_DAYS"]
|
||||
assert "дней" in result2["PERIOD_30_DAYS"]
|
||||
assert "дней" in result3["PERIOD_30_DAYS"]
|
||||
|
||||
@patch('app.localization.texts.settings')
|
||||
def test_traffic_keys_also_generated(self, mock_settings: MagicMock) -> None:
|
||||
"""Должны генерироваться не только периоды, но и ключи трафика."""
|
||||
# Настройка моков
|
||||
mock_settings.PRICE_14_DAYS = 50000
|
||||
mock_settings.PRICE_30_DAYS = 99000
|
||||
mock_settings.PRICE_60_DAYS = 189000
|
||||
mock_settings.PRICE_90_DAYS = 269000
|
||||
mock_settings.PRICE_180_DAYS = 499000
|
||||
mock_settings.PRICE_360_DAYS = 899000
|
||||
mock_settings.get_base_promo_group_period_discount.return_value = 0
|
||||
mock_settings.format_price = lambda x: f"{x // 100} ₽"
|
||||
mock_settings.PRICE_TRAFFIC_5GB = 10000
|
||||
mock_settings.PRICE_TRAFFIC_10GB = 20000
|
||||
mock_settings.PRICE_TRAFFIC_25GB = 30000
|
||||
mock_settings.PRICE_TRAFFIC_50GB = 40000
|
||||
mock_settings.PRICE_TRAFFIC_100GB = 50000
|
||||
mock_settings.PRICE_TRAFFIC_250GB = 60000
|
||||
mock_settings.PRICE_TRAFFIC_UNLIMITED = 70000
|
||||
|
||||
result = _build_dynamic_values("ru-RU")
|
||||
|
||||
# Проверяем наличие ключей трафика
|
||||
assert "TRAFFIC_5GB" in result
|
||||
assert "TRAFFIC_10GB" in result
|
||||
assert "TRAFFIC_UNLIMITED" in result
|
||||
assert "SUPPORT_INFO" in result
|
||||
Reference in New Issue
Block a user