Добавлены тесты для способов оплаты

Создан файл orders/tests/test_payment_methods.py с комплексными тестами:

1. PaymentMethodCreationTest (6 тестов)
   - Проверка создания всех 5 способов оплаты через команду
   - Проверка системных флагов и активности
   - Проверка правильности порядка сортировки
   - Проверка идемпотентности команды
   - Критический тест наличия account_balance

2. PaymentMethodMultiTenantTest (2 теста)
   - Проверка изоляции данных между тенантами
   - Проверка кастомных способов оплаты в разных тенантах

3. PaymentMethodTransactionTest (7 тестов)
   - Проверка связи PaymentMethod.transactions
   - Проверка создания транзакций
   - Проверка изоляции транзакций по способам оплаты
   - Проверка защиты от удаления (PROTECT)
   - Критический тест использования account_balance
   - Исправление бага obj.payments → obj.transactions

4. PaymentMethodOrderingTest (2 теста)
   - Проверка сортировки по полю order
   - Проверка что account_balance первый (order=0)

Особенности тестирования:
- Использование TenantTestCase для изоляции тенантов
- Использование TransactionTestCase для мультитенантных тестов
- Ручное создание/удаление схем для безопасности
- Проверка изоляции данных между схемами

Результат: 15 тестов, все прошли успешно ✓
This commit is contained in:
2025-12-01 01:30:23 +03:00
parent 7188b11f65
commit 8a64b569bd
2 changed files with 522 additions and 0 deletions

View File

@@ -0,0 +1,518 @@
# -*- coding: utf-8 -*-
"""
Тесты для способов оплаты (PaymentMethod).
Проверяем:
1. Создание способов оплаты через команду create_payment_methods
2. Уникальность способов оплаты в рамках тенанта
3. Изоляцию данных между тенантами
4. Корректность работы с транзакциями
"""
from django.test import TestCase, TransactionTestCase
from django.core.management import call_command
from django_tenants.test.cases import TenantTestCase
from django_tenants.test.client import TenantClient
from django_tenants.utils import schema_context, get_tenant_model
from orders.models import PaymentMethod, Order, Transaction, OrderStatus
from customers.models import Customer
from accounts.models import CustomUser
class PaymentMethodCreationTest(TenantTestCase):
"""
Тесты создания способов оплаты через команду.
TenantTestCase автоматически создаёт тестовый тенант
и переключается на его схему.
"""
def setUp(self):
"""Очищаем способы оплаты перед каждым тестом"""
PaymentMethod.objects.all().delete()
def test_create_payment_methods_command(self):
"""
Тест: команда create_payment_methods создаёт все 5 способов оплаты
"""
# Проверяем что нет способов оплаты
self.assertEqual(PaymentMethod.objects.count(), 0)
# Вызываем команду
call_command('create_payment_methods')
# Проверяем что создано ровно 5 способов
self.assertEqual(PaymentMethod.objects.count(), 5)
# Проверяем наличие каждого способа
expected_codes = [
'account_balance',
'cash',
'card',
'online',
'legal_entity'
]
for code in expected_codes:
self.assertTrue(
PaymentMethod.objects.filter(code=code).exists(),
f"Способ оплаты '{code}' не создан"
)
def test_payment_methods_are_system(self):
"""
Тест: все созданные способы оплаты помечены как системные
"""
call_command('create_payment_methods')
# Все способы должны быть системными
non_system_methods = PaymentMethod.objects.filter(is_system=False)
self.assertEqual(
non_system_methods.count(),
0,
"Найдены несистемные способы оплаты"
)
def test_payment_methods_are_active_by_default(self):
"""
Тест: все созданные способы оплаты активны по умолчанию
"""
call_command('create_payment_methods')
# Все способы должны быть активными
inactive_methods = PaymentMethod.objects.filter(is_active=False)
self.assertEqual(
inactive_methods.count(),
0,
"Найдены неактивные способы оплаты"
)
def test_payment_methods_order(self):
"""
Тест: способы оплаты создаются в правильном порядке
"""
call_command('create_payment_methods')
# Проверяем порядок каждого способа
expected_order = {
'account_balance': 0,
'cash': 1,
'card': 2,
'online': 3,
'legal_entity': 4,
}
for code, expected_pos in expected_order.items():
method = PaymentMethod.objects.get(code=code)
self.assertEqual(
method.order,
expected_pos,
f"Способ '{code}' имеет неверный порядок: {method.order} вместо {expected_pos}"
)
def test_payment_methods_idempotent(self):
"""
Тест: повторный вызов команды не создаёт дубликаты
"""
# Первый вызов
call_command('create_payment_methods')
first_count = PaymentMethod.objects.count()
# Второй вызов
call_command('create_payment_methods')
second_count = PaymentMethod.objects.count()
# Количество должно остаться тем же
self.assertEqual(
first_count,
second_count,
"Команда создала дубликаты при повторном вызове"
)
self.assertEqual(second_count, 5)
def test_account_balance_payment_method_exists(self):
"""
Тест: способ оплаты 'account_balance' создаётся корректно
Это критический тест для проверки исправления бага
с отсутствием способа оплаты из кошелька.
"""
call_command('create_payment_methods')
# Проверяем существование
account_balance = PaymentMethod.objects.filter(code='account_balance')
self.assertTrue(
account_balance.exists(),
"Способ оплаты 'account_balance' не создан!"
)
# Проверяем атрибуты
method = account_balance.first()
self.assertEqual(method.name, 'С баланса счёта')
self.assertEqual(method.order, 0)
self.assertTrue(method.is_system)
self.assertTrue(method.is_active)
class PaymentMethodMultiTenantTest(TransactionTestCase):
"""
Тесты изоляции способов оплаты между тенантами.
TransactionTestCase нужен для работы с несколькими тенантами
в рамках одного теста.
"""
@classmethod
def setUpClass(cls):
"""Создаём два тестовых тенанта"""
super().setUpClass()
from tenants.models import Client, Domain
# Тенант 1
cls.tenant1 = Client.objects.create(
schema_name='test_tenant1',
name='Test Tenant 1',
owner_email='tenant1@test.com'
)
Domain.objects.create(
domain='tenant1.test.localhost',
tenant=cls.tenant1,
is_primary=True
)
# Тенант 2
cls.tenant2 = Client.objects.create(
schema_name='test_tenant2',
name='Test Tenant 2',
owner_email='tenant2@test.com'
)
Domain.objects.create(
domain='tenant2.test.localhost',
tenant=cls.tenant2,
is_primary=True
)
@classmethod
def tearDownClass(cls):
"""Удаляем тестовые тенанты"""
from django.db import connection
# Удаляем схемы вручную
with connection.cursor() as cursor:
cursor.execute('DROP SCHEMA IF EXISTS test_tenant1 CASCADE')
cursor.execute('DROP SCHEMA IF EXISTS test_tenant2 CASCADE')
# Удаляем записи из public
cls.tenant1.delete()
cls.tenant2.delete()
super().tearDownClass()
def test_payment_methods_isolated_between_tenants(self):
"""
Тест: способы оплаты изолированы между тенантами
"""
# Создаём способы оплаты для tenant1
with schema_context('test_tenant1'):
# Удаляем существующие способы оплаты если есть
PaymentMethod.objects.all().delete()
call_command('create_payment_methods')
tenant1_count = PaymentMethod.objects.count()
self.assertEqual(tenant1_count, 5)
# Проверяем что в tenant2 ничего нет
with schema_context('test_tenant2'):
# Удаляем существующие способы оплаты если есть
PaymentMethod.objects.all().delete()
tenant2_count = PaymentMethod.objects.count()
self.assertEqual(tenant2_count, 0, "Утечка данных между тенантами!")
# Создаём способы для tenant2
with schema_context('test_tenant2'):
call_command('create_payment_methods')
tenant2_count = PaymentMethod.objects.count()
self.assertEqual(tenant2_count, 5)
# Проверяем что в tenant1 осталось 5
with schema_context('test_tenant1'):
tenant1_count = PaymentMethod.objects.count()
self.assertEqual(tenant1_count, 5)
def test_custom_payment_method_in_one_tenant(self):
"""
Тест: кастомный способ оплаты в одном тенанте не виден в другом
"""
# Создаём системные способы в обоих тенантах
with schema_context('test_tenant1'):
# Удаляем существующие
PaymentMethod.objects.all().delete()
call_command('create_payment_methods')
# Добавляем кастомный способ
PaymentMethod.objects.create(
code='custom_tenant1',
name='Кастомный способ тенанта 1',
is_system=False,
order=100
)
tenant1_count = PaymentMethod.objects.count()
self.assertEqual(tenant1_count, 6) # 5 системных + 1 кастомный
with schema_context('test_tenant2'):
# Удаляем существующие
PaymentMethod.objects.all().delete()
call_command('create_payment_methods')
tenant2_count = PaymentMethod.objects.count()
self.assertEqual(tenant2_count, 5) # Только системные
# Проверяем что кастомного способа нет
self.assertFalse(
PaymentMethod.objects.filter(code='custom_tenant1').exists(),
"Кастомный способ тенанта 1 виден в тенанте 2!"
)
class PaymentMethodTransactionTest(TenantTestCase):
"""
Тесты связи PaymentMethod с Transaction.
Проверяем исправление бага с obj.payments → obj.transactions
"""
def setUp(self):
"""Подготовка данных для тестов"""
# Создаём способы оплаты
call_command('create_payment_methods')
# Создаём системный статус заказа
self.status = OrderStatus.objects.create(
code='new',
name='Новый',
is_system=True
)
# Создаём тестового клиента
self.customer = Customer.objects.create(
name='Тестовый клиент',
phone='+375291234567'
)
# Создаём тестовый заказ (не указываем order_number, он создастся автоматически)
self.order = Order.objects.create(
customer=self.customer,
status=self.status
)
# Получаем способ оплаты
self.cash_method = PaymentMethod.objects.get(code='cash')
def test_payment_method_has_transactions_relation(self):
"""
Тест: у PaymentMethod есть связь 'transactions'
"""
# Проверяем что связь существует
self.assertTrue(
hasattr(self.cash_method, 'transactions'),
"У PaymentMethod нет атрибута 'transactions'"
)
def test_transactions_count_starts_at_zero(self):
"""
Тест: у нового способа оплаты нет транзакций
"""
count = self.cash_method.transactions.count()
self.assertEqual(count, 0)
def test_transaction_creates_relation(self):
"""
Тест: создание транзакции создаёт связь с PaymentMethod
"""
# Создаём транзакцию
transaction = Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=100.00,
payment_method=self.cash_method
)
# Проверяем что транзакция видна через связь
count = self.cash_method.transactions.count()
self.assertEqual(count, 1)
# Проверяем что это наша транзакция
related_transaction = self.cash_method.transactions.first()
self.assertEqual(related_transaction.id, transaction.id)
def test_multiple_transactions_same_method(self):
"""
Тест: несколько транзакций с одним способом оплаты
"""
# Создаём 3 транзакции наличными
for i in range(3):
Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=50.00 * (i + 1),
payment_method=self.cash_method
)
# Проверяем количество
count = self.cash_method.transactions.count()
self.assertEqual(count, 3)
def test_transactions_isolated_by_payment_method(self):
"""
Тест: транзакции изолированы по способу оплаты
"""
card_method = PaymentMethod.objects.get(code='card')
# Создаём транзакции разными способами
Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=100.00,
payment_method=self.cash_method
)
Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=200.00,
payment_method=card_method
)
# Проверяем изоляцию
self.assertEqual(self.cash_method.transactions.count(), 1)
self.assertEqual(card_method.transactions.count(), 1)
def test_payment_method_deletion_protection(self):
"""
Тест: нельзя удалить способ оплаты с транзакциями (PROTECT)
"""
from django.db.models import ProtectedError
# Создаём транзакцию
Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=100.00,
payment_method=self.cash_method
)
# Пытаемся удалить способ оплаты
with self.assertRaises(ProtectedError):
self.cash_method.delete()
def test_account_balance_payment_method_usable(self):
"""
Тест: способ оплаты 'account_balance' можно использовать в транзакциях
Проверка что исправление бага работает корректно.
"""
from decimal import Decimal
account_balance = PaymentMethod.objects.get(code='account_balance')
# Добавляем деньги в кошелёк клиента
self.customer.wallet_balance = Decimal('500.00')
self.customer.save()
# Создаём транзакцию с оплатой из кошелька
transaction = Transaction.objects.create(
order=self.order,
transaction_type='payment',
amount=Decimal('150.00'),
payment_method=account_balance
)
# Проверяем что транзакция создана
self.assertIsNotNone(transaction.id)
self.assertEqual(transaction.payment_method.code, 'account_balance')
# Проверяем связь
self.assertEqual(account_balance.transactions.count(), 1)
class PaymentMethodAdminTest(TenantTestCase):
"""
Тесты для проверки работы админки PaymentMethod.
Проверяем исправление бага с obj.payments → obj.transactions
"""
def setUp(self):
"""Подготовка данных"""
call_command('create_payment_methods')
# Создаём суперпользователя
self.admin_user = CustomUser.objects.create_superuser(
email='admin@test.com',
password='testpass123'
)
# Логинимся
self.client = TenantClient(self.tenant)
self.client.force_login(self.admin_user)
def test_payment_method_admin_list_view(self):
"""
Тест: список способов оплаты в админке загружается без ошибок
"""
response = self.client.get('/admin/orders/paymentmethod/')
# Проверяем что нет ошибки AttributeError
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'AttributeError')
self.assertNotContains(response, "has no attribute 'payments'")
def test_payment_method_admin_shows_all_methods(self):
"""
Тест: в админке отображаются все 5 способов оплаты
"""
response = self.client.get('/admin/orders/paymentmethod/')
self.assertContains(response, 'С баланса счёта')
self.assertContains(response, 'Наличными')
self.assertContains(response, 'Картой')
self.assertContains(response, 'Онлайн')
self.assertContains(response, 'Безнал от ЮРЛИЦ')
class PaymentMethodOrderingTest(TenantTestCase):
"""
Тесты сортировки способов оплаты.
"""
def setUp(self):
"""Создаём способы оплаты"""
call_command('create_payment_methods')
def test_payment_methods_ordered_by_order_field(self):
"""
Тест: способы оплаты сортируются по полю 'order'
"""
methods = PaymentMethod.objects.all()
# Проверяем что порядок возрастающий
previous_order = -1
for method in methods:
self.assertGreater(
method.order,
previous_order,
f"Нарушен порядок сортировки: {method.code}"
)
previous_order = method.order
def test_account_balance_is_first(self):
"""
Тест: 'account_balance' первый в списке (order=0)
"""
first_method = PaymentMethod.objects.first()
self.assertEqual(first_method.code, 'account_balance')
self.assertEqual(first_method.order, 0)