feat(events): harden registration checkout lifecycle
This commit is contained in:
@@ -168,7 +168,8 @@ class TestPaymentsAPIIntegrationTests:
|
||||
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
|
||||
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
||||
assert 'status=' not in response.url
|
||||
|
||||
def test_callback_failure_redirects_failed(self, mocker):
|
||||
mock_post = mocker.patch('apps.payments.api.views.requests.post')
|
||||
@@ -179,11 +180,21 @@ class TestPaymentsAPIIntegrationTests:
|
||||
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
|
||||
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
||||
|
||||
def test_callback_missing_authority_returns_error(self):
|
||||
def test_callback_missing_authority_redirects_to_safe_unknown_result(self):
|
||||
response = self.client.get('/api/payments/callback', {'Status': 'OK'})
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 302
|
||||
assert '/events?payment_callback=unmatched' in response.url
|
||||
|
||||
def test_callback_unknown_authority_redirects_to_safe_unknown_result(self):
|
||||
response = self.client.get(
|
||||
'/api/payments/callback',
|
||||
{'Authority': 'AUTH-UNKNOWN', 'Status': 'OK'},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert '/events?payment_callback=unmatched' in response.url
|
||||
assert 'AUTH-UNKNOWN' not in response.url
|
||||
|
||||
def test_callback_not_ok_is_verified_before_being_canceled(self, mocker):
|
||||
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')
|
||||
@@ -191,8 +202,8 @@ class TestPaymentsAPIIntegrationTests:
|
||||
gateway.return_value.json.return_value = {'data': {'code': -51}}
|
||||
response = self.client.get('/api/payments/callback', {'Authority': 'AUTH789', 'Status': 'NOK'})
|
||||
payment.refresh_from_db()
|
||||
assert payment.status == Payment.OrderStatusChoices.FAILED
|
||||
assert 'status=failed' in response.url
|
||||
assert payment.status == Payment.OrderStatusChoices.CANCELED
|
||||
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
||||
|
||||
def test_late_canceled_callback_does_not_overwrite_verified_payment(self, mocker):
|
||||
registration = Registration.objects.create(
|
||||
@@ -229,8 +240,9 @@ class TestPaymentsAPIIntegrationTests:
|
||||
assert payment.status == Payment.OrderStatusChoices.PAID
|
||||
assert payment.verified_at is not None
|
||||
assert registration.status == Registration.StatusChoices.CONFIRMED
|
||||
assert 'status=success' in success_response.url
|
||||
assert 'status=success' in late_failure_response.url
|
||||
expected_return = f'/events/{self.event.slug}/registration?registration_id={registration.ticket_id}'
|
||||
assert expected_return in success_response.url
|
||||
assert expected_return in late_failure_response.url
|
||||
|
||||
def test_create_payment_gateway_timeout_stays_pending(self, mocker):
|
||||
mocker.patch('apps.payments.api.views.requests.post', side_effect=RuntimeError('down'))
|
||||
@@ -249,8 +261,7 @@ class TestPaymentsAPIIntegrationTests:
|
||||
|
||||
payment.refresh_from_db()
|
||||
assert response.status_code == 302
|
||||
assert 'status=pending' in response.url
|
||||
assert f'payment_id={payment.id}' in response.url
|
||||
assert f'/events/{self.event.slug}/registration?payment_id={payment.id}' in response.url
|
||||
assert payment.status == Payment.OrderStatusChoices.PENDING
|
||||
reconcile.assert_called_once_with(args=(payment.id,), countdown=60)
|
||||
notify.assert_not_called()
|
||||
@@ -261,12 +272,45 @@ class TestPaymentsAPIIntegrationTests:
|
||||
assert own.status_code == 200
|
||||
assert own.json()['payment_id'] == payment.id
|
||||
assert own.json()['status'] == 'pending'
|
||||
assert own.json()['verification_state'] == 'awaiting_verification'
|
||||
assert own.json()['verification_attempts'] == 0
|
||||
assert own.json()['last_verification_at'] is None
|
||||
assert own.json()['manual_review_at'] is None
|
||||
|
||||
other = User.objects.create_user(username='other_payment_owner', email='other-payment@example.com', mobile='09123456780', password=self.password)
|
||||
other_token = create_jwt_token(other)
|
||||
forbidden = self.client.get(f'/api/payments/status/{payment.id}', HTTP_AUTHORIZATION=f'Bearer {other_token}')
|
||||
assert forbidden.status_code == 404
|
||||
|
||||
def test_payment_reference_lookup_requires_authentication_and_is_user_scoped(self):
|
||||
payment = Payment.objects.create(
|
||||
user=self.user,
|
||||
event=self.event,
|
||||
base_amount=self.event.price,
|
||||
amount=self.event.price,
|
||||
status=Payment.OrderStatusChoices.PAID,
|
||||
ref_id="OWNER-ONLY-REF",
|
||||
)
|
||||
|
||||
unauthenticated = self.client.get("/api/payments/by-ref/OWNER-ONLY-REF")
|
||||
owned = self.client.get("/api/payments/by-ref/OWNER-ONLY-REF", **self._headers())
|
||||
other = User.objects.create_user(
|
||||
username="other_ref_owner",
|
||||
email="other-ref-owner@example.com",
|
||||
mobile="09123456781",
|
||||
password=self.password,
|
||||
)
|
||||
other_token = create_jwt_token(other)
|
||||
forbidden = self.client.get(
|
||||
"/api/payments/by-ref/OWNER-ONLY-REF",
|
||||
HTTP_AUTHORIZATION=f"Bearer {other_token}",
|
||||
)
|
||||
|
||||
assert unauthenticated.status_code == 401
|
||||
assert owned.status_code == 200
|
||||
assert owned.json()["payment_id"] == payment.id
|
||||
assert forbidden.status_code == 404
|
||||
|
||||
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())
|
||||
@@ -341,10 +385,12 @@ def test_my_payments_are_user_scoped_and_omit_gateway_identifiers(client, user_f
|
||||
payload = response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]["event"]["title"] == own_event.title
|
||||
assert payload[0]["event"]["id"] == own_event.id
|
||||
assert payload[0]["amount"] == 100000
|
||||
assert payload[0]["discount_amount"] == 20000
|
||||
assert payload[0]["status"] == "paid"
|
||||
assert set(payload[0]) == {"event", "base_amount", "discount_amount", "amount", "status", "created_at", "paid_at"}
|
||||
assert set(payload[0]) == {"payment_id", "event", "base_amount", "discount_amount", "amount", "status", "refund_status", "created_at", "paid_at"}
|
||||
assert payload[0]["payment_id"] == Payment.objects.get(user=current_user, event=own_event).id
|
||||
assert "internal-authority-owner" not in response.content.decode()
|
||||
assert "internal-reference-owner" not in response.content.decode()
|
||||
assert "internal-card-hash" not in response.content.decode()
|
||||
|
||||
Reference in New Issue
Block a user