Files
guilan-ace-backend/apps/payments/tests/integration/test_payment_models.py
Amirhossein Khalili ff99130938 test: migrate backend suites to pytest with module coverage gate
Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
2026-09-26 16:50:29 +03:30

149 lines
6.5 KiB
Python

import pytest
import uuid
from datetime import timedelta
from types import SimpleNamespace
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.contrib.admin import AdminSite
from apps.payments.admin import DiscountCodeAdmin
from apps.payments.models import DiscountCode, Payment
from apps.payments.resources import DiscountResource, PaymentResource
from apps.events.models import Event
from apps.users.models import User
from ninja.errors import HttpError
class PaymentTestMixin:
@staticmethod
def _create_user(**overrides):
data = {'username': f'user_{uuid.uuid4().hex[:6]}', 'email': f'user_{uuid.uuid4().hex[:6]}@example.com', 'mobile': f'09{uuid.uuid4().int % 1000000000:09d}', 'password': 'Test!1234'}
data.update(overrides)
return User.objects.create_user(**data)
@staticmethod
def _create_event(**overrides):
now = timezone.now()
defaults = {'title': 'Sample', 'description': 'Desc', 'start_time': now, 'end_time': now + timedelta(hours=2), 'registration_start_date': now - timedelta(days=1), 'registration_end_date': now + timedelta(days=5), 'slug': f'event-{uuid.uuid4().hex[:6]}', 'price': 100000, 'capacity': 10, 'status': Event.StatusChoices.PUBLISHED}
defaults.update(overrides)
return Event.objects.create(**defaults)
@staticmethod
def _discount_code(**overrides):
defaults = {'code': f'CODE{uuid.uuid4().hex[:4]}', 'value': 50, 'is_active': True, 'type': DiscountCode.Type.PERCENT}
defaults.update(overrides)
return DiscountCode.objects.create(**defaults)
@pytest.mark.django_db
@pytest.mark.integration
class TestDiscountCodeModelTests(PaymentTestMixin):
def setup_method(self):
self.event = self._create_event()
self.user = self._create_user(is_email_verified=True)
def test_zero_price_returns_zero_discount(self):
event = self._create_event(price=0)
code = self._discount_code()
code.applicable_events.add(event)
assert code.calculate_discount(event, self.user) == (0, 0)
def test_inactive_raises_error(self):
code = self._discount_code(is_active=False)
code.applicable_events.add(self.event)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_start_date_validation(self):
code = self._discount_code(starts_at=timezone.now() + timedelta(days=1))
code.applicable_events.add(self.event)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_end_date_validation(self):
code = self._discount_code(ends_at=timezone.now() - timedelta(days=1))
code.applicable_events.add(self.event)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_applicable_events_enforcement(self):
code = self._discount_code()
other_event = self._create_event()
code.applicable_events.add(other_event)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_min_amount_guard(self):
code = self._discount_code(min_amount=200000)
code.applicable_events.add(self.event)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_usage_limit_total(self):
code = self._discount_code(usage_limit_total=1)
code.applicable_events.add(self.event)
Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, discount_amount=0, status=Payment.OrderStatusChoices.PAID, discount_code=code)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_usage_limit_per_user(self):
code = self._discount_code(usage_limit_per_user=1)
code.applicable_events.add(self.event)
Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, discount_amount=0, status=Payment.OrderStatusChoices.PENDING, discount_code=code)
with pytest.raises(HttpError):
code.calculate_discount(self.event, self.user)
def test_final_price_below_min_post_discount(self):
event = self._create_event(price=15000)
code = self._discount_code(value=80)
code.applicable_events.add(event)
with pytest.raises(HttpError):
code.calculate_discount(event, self.user)
def test_fixed_discount_type(self):
code = self._discount_code(type=DiscountCode.Type.FIXED, value=5000)
code.applicable_events.add(self.event)
final, disc = code.calculate_discount(self.event, self.user)
assert disc == 5000
assert final == self.event.price - 5000
@pytest.mark.django_db
@pytest.mark.integration
class TestPaymentModelAndResourceTests(PaymentTestMixin):
def setup_method(self):
self.event = self._create_event()
self.user = self._create_user(is_email_verified=True)
def test_payment_clean_validates_amount(self):
payment = Payment(user=self.user, event=self.event, base_amount=1000, amount=500, discount_amount=400, status=Payment.OrderStatusChoices.INIT)
with pytest.raises(ValidationError):
payment.full_clean()
def test_payment_resource_defers_user_event(self):
payment = Payment.objects.create(user=self.user, event=self.event, base_amount=1000, amount=1000, discount_amount=0, status=Payment.OrderStatusChoices.INIT)
resource = PaymentResource()
user_cell = resource.fields['user'].widget.clean(self.user.username, None)
assert user_cell == self.user
event_cell = resource.fields['event'].widget.clean(self.event.title, None)
assert event_cell == self.event
def test_discount_resource_expands_events(self):
resource = DiscountResource()
widget = resource.fields['event'].widget
assert widget.separator == '||'
@pytest.mark.django_db
@pytest.mark.integration
class TestDiscountCodeAdminTests(PaymentTestMixin):
def setup_method(self):
self.admin = DiscountCodeAdmin(DiscountCode, AdminSite())
def test_deactivate_codes_action(self):
code = self._discount_code()
queryset = DiscountCode.objects.filter(pk=code.pk)
request = SimpleNamespace(_messages=SimpleNamespace(add=lambda *args, **kwargs: None))
self.admin.deactivate_codes(request, queryset)
code.refresh_from_db()
assert not code.is_active