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.
This commit is contained in:
2026-09-26 16:50:29 +03:30
parent 3ec28638d1
commit ff99130938
76 changed files with 3271 additions and 1357 deletions

View File

@@ -0,0 +1,87 @@
import json
import pytest
from apps.events.models import Registration
from apps.payments.models import DiscountCode, Payment
pytestmark = [pytest.mark.integration, pytest.mark.django_db]
def _send(client, method, path, headers, payload=None):
kwargs = dict(headers)
if payload is not None:
kwargs.update(data=json.dumps(payload), content_type="application/json")
return getattr(client, method)(path, **kwargs)
def test_discount_code_admin_crud_and_restore(client, user_factory, auth_headers, event_factory):
staff = user_factory(is_staff=True)
admin = user_factory(is_staff=True, is_superuser=True)
member = user_factory()
event = event_factory(price=10000)
path = "/api/payments/admin/discount-codes"
payload = {"code": "pytest10", "type": "percent", "value": 10, "applicable_event_ids": [event.pk]}
assert _send(client, "get", path, auth_headers(member)).status_code == 403
assert _send(client, "post", path, auth_headers(member), payload).status_code == 403
created = _send(client, "post", path, auth_headers(staff), payload)
assert created.status_code == 201, created.content
code = DiscountCode.objects.get(pk=created.json()["id"])
assert code.code == "PYTEST10" and list(code.applicable_events.all()) == [event]
assert _send(client, "post", path, auth_headers(staff), payload).status_code == 400
assert _send(client, "get", path + "?search=PYTEST10", auth_headers(staff)).json()["count"] == 1
changed = _send(client, "put", f"{path}/{code.pk}", auth_headers(staff), {**payload, "value": 20})
assert changed.status_code == 200, changed.content
code.refresh_from_db()
assert code.value == 20
assert _send(client, "delete", f"{path}/{code.pk}", auth_headers(staff)).status_code == 403
assert _send(client, "delete", f"{path}/{code.pk}", auth_headers(admin)).status_code == 200
assert DiscountCode.deleted_objects.filter(pk=code.pk).exists()
assert _send(client, "post", f"{path}/{code.pk}/restore", auth_headers(admin)).status_code == 200
assert DiscountCode.objects.filter(pk=code.pk).exists()
@pytest.mark.parametrize("gateway_result,expected", [({"errors": {"code": -9}}, 502), (None, 502)])
def test_payment_create_gateway_failures_leave_no_active_payment(client, user_factory, auth_headers, event_factory,
mocker, settings, gateway_result, expected):
settings.ZARINPAL_MERCHANT_ID = "test"
settings.ZARINPAL_REQUEST_URL = "https://gateway.example/request"
user = user_factory()
event = event_factory(price=25000)
gateway = mocker.patch("apps.payments.api.views.requests.post")
if gateway_result is None:
gateway.side_effect = RuntimeError("offline")
else:
gateway.return_value.json.return_value = gateway_result
response = _send(client, "post", "/api/payments/create", auth_headers(user),
{"event_id": event.pk, "description": "Ticket"})
assert response.status_code == expected
assert not Payment.objects.filter(user=user, event=event).exists()
assert Registration.objects.filter(user=user, event=event).exists()
@pytest.mark.parametrize("gateway_result", [None, {"data": {"code": -1}}])
def test_payment_callback_verification_failures_notify_once(client, user_factory, event_factory, mocker,
settings, gateway_result):
settings.ZARINPAL_MERCHANT_ID = "test"
settings.ZARINPAL_VERIFY_URL = "https://gateway.example/verify"
settings.FRONTEND_CALLBACK_URL = "https://frontend.example/result"
user = user_factory()
event = event_factory(price=25000)
registration = Registration.objects.create(event=event, user=user, status=Registration.StatusChoices.PENDING)
payment = Payment.objects.create(user=user, event=event, registration=registration, amount=25000,
base_amount=25000, authority="AUTH-FAIL", status=Payment.OrderStatusChoices.PENDING)
gateway = mocker.patch("apps.payments.api.views.requests.post")
notify = mocker.patch("apps.payments.api.views.notify_user")
if gateway_result is None:
gateway.side_effect = RuntimeError("offline")
else:
gateway.return_value.json.return_value = gateway_result
response = client.get("/api/payments/callback", {"Authority": "AUTH-FAIL", "Status": "OK"})
assert response.status_code == 302 and "status=failed" in response.url
payment.refresh_from_db()
registration.refresh_from_db()
assert payment.status == Payment.OrderStatusChoices.FAILED
assert registration.status == Registration.StatusChoices.PENDING
notify.assert_called_once()

View File

@@ -1,11 +1,9 @@
import pytest
import uuid
from datetime import timedelta
from types import SimpleNamespace
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from django.contrib.admin import AdminSite
from apps.payments.admin import DiscountCodeAdmin
from apps.payments.models import DiscountCode, Payment
@@ -14,50 +12,32 @@ 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",
"password": "Test!1234",
}
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 = {'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 = {'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):
class DiscountCodeModelTests(TestCase, PaymentTestMixin):
def setUp(self):
def setup_method(self):
self.event = self._create_event()
self.user = self._create_user(is_email_verified=True)
@@ -65,124 +45,98 @@ class DiscountCodeModelTests(TestCase, PaymentTestMixin):
event = self._create_event(price=0)
code = self._discount_code()
code.applicable_events.add(event)
self.assertEqual(code.calculate_discount(event, self.user), (0, 0))
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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 self.assertRaises(HttpError):
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)
self.assertEqual(disc, 5000)
self.assertEqual(final, self.event.price - 5000)
assert disc == 5000
assert final == self.event.price - 5000
@pytest.mark.django_db
@pytest.mark.integration
class TestPaymentModelAndResourceTests(PaymentTestMixin):
class PaymentModelAndResourceTests(TestCase, PaymentTestMixin):
def setUp(self):
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 self.assertRaises(ValidationError):
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,
)
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)
self.assertEqual(user_cell, self.user)
event_cell = resource.fields["event"].widget.clean(self.event.title, None)
self.assertEqual(event_cell, self.event)
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
self.assertEqual(widget.separator, "||")
widget = resource.fields['event'].widget
assert widget.separator == '||'
@pytest.mark.django_db
@pytest.mark.integration
class TestDiscountCodeAdminTests(PaymentTestMixin):
class DiscountCodeAdminTests(TestCase, PaymentTestMixin):
def setUp(self):
def setup_method(self):
self.admin = DiscountCodeAdmin(DiscountCode, AdminSite())
def test_deactivate_codes_action(self):
@@ -191,4 +145,4 @@ class DiscountCodeAdminTests(TestCase, PaymentTestMixin):
request = SimpleNamespace(_messages=SimpleNamespace(add=lambda *args, **kwargs: None))
self.admin.deactivate_codes(request, queryset)
code.refresh_from_db()
self.assertFalse(code.is_active)
assert not code.is_active

View File

@@ -1,282 +1,137 @@
import pytest
import json
from datetime import timedelta
from unittest import mock
from django.test import TestCase, override_settings
from django.utils import timezone
from core.authentication import create_jwt_token
from apps.events.models import Event, Registration
from apps.payments.models import Payment, DiscountCode
from apps.users.models import User
@override_settings(
ZARINPAL_MERCHANT_ID="MID",
ZARINPAL_REQUEST_URL="https://zarinpal/request",
ZARINPAL_STARTPAY="https://zarinpal/start/",
ZARINPAL_VERIFY_URL="https://zarinpal/verify",
ZARINPAL_CALLBACK_URL="https://frontend/callback",
)
class PaymentsAPIIntegrationTests(TestCase):
password = "PaymentPass!123"
@pytest.mark.django_db
@pytest.mark.integration
class TestPaymentsAPIIntegrationTests:
password = 'PaymentPass!123'
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username="pay_user",
email="pay.user@example.com",
password=cls.password,
)
cls.user = User.objects.create_user(username='pay_user', email='pay.user@example.com', mobile='09123456789', password=cls.password)
cls.user.is_email_verified = True
cls.user.save(update_fields=["is_email_verified"])
cls.user.save(update_fields=['is_email_verified'])
def setUp(self):
super().setUp()
self.event = Event.objects.create(
title="Pay Event",
description="Payment event",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=2),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=1),
slug="pay-event",
price=50000,
capacity=10,
status=Event.StatusChoices.PUBLISHED,
)
def setup_method(self):
type(self).setUpTestData()
self.event = Event.objects.create(title='Pay Event', description='Payment event', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=2), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=1), slug='pay-event', price=50000, capacity=10, status=Event.StatusChoices.PUBLISHED)
self.token = create_jwt_token(self.user)
def _headers(self):
return {"HTTP_AUTHORIZATION": f"Bearer {self.token}"}
return {'HTTP_AUTHORIZATION': f'Bearer {self.token}'}
def _create_paid_event(self):
return Event.objects.create(
title="Paid Event",
description="Paid",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=1),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=2),
slug=f"paid-{timezone.now().timestamp()}",
price=20000,
capacity=5,
status=Event.StatusChoices.PUBLISHED,
)
return Event.objects.create(title='Paid Event', description='Paid', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=2), slug=f'paid-{timezone.now().timestamp()}', price=20000, capacity=5, status=Event.StatusChoices.PUBLISHED)
def _create_discount_code(self, event):
code = DiscountCode.objects.create(
code="DISC50",
value=50,
is_active=True,
)
code = DiscountCode.objects.create(code='DISC50', value=50, is_active=True)
code.applicable_events.add(event)
return code
def test_create_payment_for_free_event(self):
free = Event.objects.create(
title="Free",
description="Zero",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=1),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=1),
slug="free-event",
price=0,
capacity=10,
status=Event.StatusChoices.PUBLISHED,
)
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": free.id,
"description": "Free registration",
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
free = Event.objects.create(title='Free', description='Zero', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=1), slug='free-event', price=0, capacity=10, status=Event.StatusChoices.PUBLISHED)
response = self.client.post('/api/payments/create', data=json.dumps({'event_id': free.id, 'description': 'Free registration'}), content_type='application/json', **self._headers())
assert response.status_code == 200
data = response.json()
self.assertEqual(data["amount"], 0)
self.assertIsNone(data["start_pay_url"])
assert data['amount'] == 0
assert data['start_pay_url'] is None
@mock.patch("apps.payments.api.views.requests.post")
def test_create_payment_with_discount(self, mock_post):
mock_response = mock.Mock()
mock_response.json.return_value = {"data": {"code": 100, "authority": "AUTH"}}
def test_create_payment_with_discount(self, mocker):
mock_post = mocker.patch('apps.payments.api.views.requests.post')
mock_response = mocker.Mock()
mock_response.json.return_value = {'data': {'code': 100, 'authority': 'AUTH'}}
mock_post.return_value = mock_response
code = self._create_discount_code(self.event)
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": self.event.id,
"description": "Pay with discount",
"discount_code": code.code,
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Pay with discount', 'discount_code': code.code}), content_type='application/json', **self._headers())
assert response.status_code == 200
payload = response.json()
self.assertEqual(payload["discount_amount"], self.event.price // 2)
self.assertEqual(payload["amount"], self.event.price // 2)
self.assertIn("start_pay_url", payload)
assert payload['discount_amount'] == self.event.price // 2
assert payload['amount'] == self.event.price // 2
assert 'start_pay_url' in payload
payment = Payment.objects.get(user=self.user, event=self.event)
self.assertEqual(payment.discount_code, code)
assert payment.discount_code == code
@mock.patch("apps.payments.api.views.requests.post")
def test_callback_success_marks_paid(self, mock_post):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH123",
)
mock_resp = mock.Mock()
mock_resp.json.return_value = {"data": {"code": 100, "ref_id": "REF", "card_pan": "123", "card_hash": "ABC"}}
def test_callback_success_marks_paid(self, mocker):
mock_post = mocker.patch('apps.payments.api.views.requests.post')
payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH123')
mock_resp = mocker.Mock()
mock_resp.json.return_value = {'data': {'code': 100, 'ref_id': 'REF', 'card_pan': '123', 'card_hash': 'ABC'}}
mock_post.return_value = mock_resp
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH123", "Status": "OK"},
)
response = self.client.get('/api/payments/callback', {'Authority': 'AUTH123', 'Status': 'OK'})
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.PAID)
self.assertTrue("status=success" in response.url)
assert payment.status == Payment.OrderStatusChoices.PAID
assert 'status=success' in response.url
@mock.patch("apps.payments.api.views.requests.post")
def test_callback_failure_redirects_failed(self, mock_post):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH456",
)
mock_resp = mock.Mock()
mock_resp.json.return_value = {"data": {"code": 101, "ref_id": "REF"}}
def test_callback_failure_redirects_failed(self, mocker):
mock_post = mocker.patch('apps.payments.api.views.requests.post')
payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH456')
mock_resp = mocker.Mock()
mock_resp.json.return_value = {'data': {'code': 101, 'ref_id': 'REF'}}
mock_post.return_value = mock_resp
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH456", "Status": "OK"},
)
response = self.client.get('/api/payments/callback', {'Authority': 'AUTH456', 'Status': 'OK'})
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.PAID)
self.assertTrue("status=success" in response.url)
assert payment.status == Payment.OrderStatusChoices.PAID
assert 'status=success' in response.url
def test_callback_missing_authority_returns_error(self):
response = self.client.get("/api/payments/callback", {"Status": "OK"})
self.assertEqual(response.status_code, 400)
response = self.client.get('/api/payments/callback', {'Status': 'OK'})
assert response.status_code == 400
def test_callback_not_ok_cancels(self):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH789",
)
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH789", "Status": "NOK"},
)
payment = Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PENDING, authority='AUTH789')
response = self.client.get('/api/payments/callback', {'Authority': 'AUTH789', 'Status': 'NOK'})
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.CANCELED)
self.assertIn("status=failed", response.url)
assert payment.status == Payment.OrderStatusChoices.CANCELED
assert 'status=failed' in response.url
@mock.patch("apps.payments.api.views.requests.post", side_effect=RuntimeError("down"))
def test_create_payment_gateway_failure(self, mock_post):
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": self.event.id,
"description": "Gateway fail",
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 502)
self.assertFalse(Payment.objects.filter(user=self.user).exists())
def test_create_payment_gateway_failure(self, mocker):
mock_post = mocker.patch('apps.payments.api.views.requests.post', side_effect=RuntimeError('down'))
response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Gateway fail'}), content_type='application/json', **self._headers())
assert response.status_code == 502
assert not Payment.objects.filter(user=self.user).exists()
def test_create_payment_when_already_paid(self):
Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PAID,
)
response = self.client.post(
"/api/payments/create",
data=json.dumps({"event_id": self.event.id, "description": "Duplicate"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 400)
Payment.objects.create(user=self.user, event=self.event, base_amount=self.event.price, amount=self.event.price, status=Payment.OrderStatusChoices.PAID)
response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Duplicate'}), content_type='application/json', **self._headers())
assert response.status_code == 400
@mock.patch("apps.payments.api.views.requests.post")
def test_registration_final_price_none_updates(self, mock_post):
registration = Registration.objects.create(
event=self.event,
user=self.user,
status=Registration.StatusChoices.PENDING,
final_price=None,
)
mock_response = mock.Mock()
mock_response.json.return_value = {"data": {"code": 100, "authority": "AUTH"}}
def test_registration_final_price_none_updates(self, mocker):
mock_post = mocker.patch('apps.payments.api.views.requests.post')
registration = Registration.objects.create(event=self.event, user=self.user, status=Registration.StatusChoices.PENDING, final_price=None)
mock_response = mocker.Mock()
mock_response.json.return_value = {'data': {'code': 100, 'authority': 'AUTH'}}
mock_post.return_value = mock_response
response = self.client.post(
"/api/payments/create",
data=json.dumps({"event_id": self.event.id, "description": "Update"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
response = self.client.post('/api/payments/create', data=json.dumps({'event_id': self.event.id, 'description': 'Update'}), content_type='application/json', **self._headers())
assert response.status_code == 200
registration.refresh_from_db()
if registration.final_price is None:
self.fail("final_price should be populated")
pytest.fail('final_price should be populated')
def test_coupon_check_success_and_errors(self):
code = DiscountCode.objects.create(code="PAYCO", value=20, is_active=True, type=DiscountCode.Type.PERCENT)
code = DiscountCode.objects.create(code='PAYCO', value=20, is_active=True, type=DiscountCode.Type.PERCENT)
code.applicable_events.add(self.event)
missing = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id}), content_type='application/json', **self._headers())
assert missing.status_code == 422
invalid = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id, 'code': 'INVALID'}), content_type='application/json', **self._headers())
assert invalid.status_code == 404
success = self.client.post('/api/payments/coupon/check', data=json.dumps({'event_id': self.event.id, 'code': code.code}), content_type='application/json', **self._headers())
assert success.status_code == 200
assert 'final_price' in success.json()
# missing code
missing = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(missing.status_code, 422)
@pytest.fixture(autouse=True)
def _pytest_client(self, client):
self.client = client
# invalid code
invalid = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id, "code": "INVALID"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(invalid.status_code, 404)
success = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id, "code": code.code}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(success.status_code, 200)
self.assertIn("final_price", success.json())
@pytest.fixture(autouse=True)
def _pytest_settings(self, settings):
settings.ZARINPAL_MERCHANT_ID = 'MID'
settings.ZARINPAL_REQUEST_URL = 'https://zarinpal/request'
settings.ZARINPAL_STARTPAY = 'https://zarinpal/start/'
settings.ZARINPAL_VERIFY_URL = 'https://zarinpal/verify'
settings.ZARINPAL_CALLBACK_URL = 'https://frontend/callback'

View File

@@ -0,0 +1,21 @@
import pytest
from pydantic import ValidationError
from apps.payments.api.schemas import CreatePaymentIn, DiscountCodeWriteSchema
pytestmark = pytest.mark.unit
def test_payment_request_requires_event_and_description():
with pytest.raises(ValidationError):
CreatePaymentIn(event_id=1)
payload = CreatePaymentIn(event_id=1, description="Workshop")
assert payload.discount_code is None
def test_discount_write_defaults_to_active_percentage_code():
payload = DiscountCodeWriteSchema(code="SAVE", value=10)
assert payload.type == "percent"
assert payload.is_active
assert payload.applicable_event_ids == []