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.
138 lines
8.6 KiB
Python
138 lines
8.6 KiB
Python
import pytest
|
|
import json
|
|
from datetime import timedelta
|
|
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
|
|
|
|
@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', mobile='09123456789', password=cls.password)
|
|
cls.user.is_email_verified = True
|
|
cls.user.save(update_fields=['is_email_verified'])
|
|
|
|
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}'}
|
|
|
|
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)
|
|
|
|
def _create_discount_code(self, event):
|
|
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())
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['amount'] == 0
|
|
assert data['start_pay_url'] is None
|
|
|
|
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())
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
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)
|
|
assert payment.discount_code == code
|
|
|
|
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'})
|
|
payment.refresh_from_db()
|
|
assert payment.status == Payment.OrderStatusChoices.PAID
|
|
assert 'status=success' in response.url
|
|
|
|
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'})
|
|
payment.refresh_from_db()
|
|
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'})
|
|
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.refresh_from_db()
|
|
assert payment.status == Payment.OrderStatusChoices.CANCELED
|
|
assert 'status=failed' in response.url
|
|
|
|
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())
|
|
assert response.status_code == 400
|
|
|
|
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())
|
|
assert response.status_code == 200
|
|
registration.refresh_from_db()
|
|
if registration.final_price is None:
|
|
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.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()
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _pytest_client(self, client):
|
|
self.client = client
|
|
|
|
@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'
|