Files
octopus/myproject/orders/tests/test_payment_methods.py
Andrey Smakotin 8a64b569bd Добавлены тесты для способов оплаты
Создан файл 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 тестов, все прошли успешно ✓
2025-12-01 01:30:23 +03:00

519 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)