Signed payment links, hashed refresh tokens, login-only leaderboard, load test tools
- /api/payment/orders/{id}/pay requires the short-lived signature that paymentUrl carries;
without it (or with another order's) it is the same 404 as an unknown order
- Session.refresh_token stores the SHA-256 of the token; migration accounts.0007 hashes the
existing rows so tokens clients already hold keep working
- /api/leaderboard requires a token (guests have one); names of children were scrapable
- RAG mock only for `manage.py test`, not any command with an argument named "test"
- tools/loadtest: user factory, load generator, fake GapGPT+Qdrant, with method notes
- security-before-production.md lists only open items; closed ones become a guards table
with the tests that hold them. Comments describe current behaviour, not history
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import importlib
|
||||
import os
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.core.cache import cache
|
||||
@@ -14,7 +15,7 @@ from . import bazaar, zarinpal
|
||||
from .bazaar import BazaarPurchaseResult
|
||||
from .models import Order, Transaction
|
||||
from .permissions import HasInternalServiceKey
|
||||
from .services import call_payment_gateway
|
||||
from .services import PAY_LINK_MAX_AGE_SECONDS, call_payment_gateway, pay_link_token
|
||||
|
||||
|
||||
class SecretDefaultsFailClosedTests(TestCase):
|
||||
@@ -467,7 +468,13 @@ class CallPaymentGatewayTests(TestCase):
|
||||
|
||||
url = call_payment_gateway(order)
|
||||
|
||||
self.assertEqual(url, f'https://api.tadrisino.ir/api/payment/orders/{order.id}/pay')
|
||||
self.assertTrue(url.startswith(f'https://api.tadrisino.ir/api/payment/orders/{order.id}/pay?t='))
|
||||
|
||||
|
||||
def _pay_path(order):
|
||||
"""همان paymentUrlی که کلاینت میگیرد، بیدامنه."""
|
||||
url = call_payment_gateway(order)
|
||||
return url[url.index('/api/'):]
|
||||
|
||||
|
||||
class PaymentRedirectViewTests(TestCase):
|
||||
@@ -495,14 +502,14 @@ class PaymentRedirectViewTests(TestCase):
|
||||
mock_request_payment.return_value = zarinpal.ZarinpalRequestResult(
|
||||
authority='A-ref', payment_url='https://www.zarinpal.com/pg/StartPay/A-ref',
|
||||
)
|
||||
response = self.client.get(f'/api/payment/orders/{self.order.id}/pay')
|
||||
response = self.client.get(_pay_path(self.order))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Referrer-Policy'], 'strict-origin-when-cross-origin')
|
||||
self.assertEqual(self.client.get('/api/health')['Referrer-Policy'], 'same-origin')
|
||||
|
||||
@override_settings(ZARINPAL_MERCHANT_ID='', ZARINPAL_CALLBACK_URL='')
|
||||
def test_unconfigured_gateway_returns_503(self):
|
||||
response = self.client.get(f'/api/payment/orders/{self.order.id}/pay')
|
||||
response = self.client.get(_pay_path(self.order))
|
||||
self.assertEqual(response.status_code, 503)
|
||||
|
||||
@override_settings(
|
||||
@@ -515,7 +522,7 @@ class PaymentRedirectViewTests(TestCase):
|
||||
authority='A-first', payment_url='https://www.zarinpal.com/pg/StartPay/A-first',
|
||||
)
|
||||
|
||||
response = self.client.get(f'/api/payment/orders/{self.order.id}/pay')
|
||||
response = self.client.get(_pay_path(self.order))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, 'https://www.zarinpal.com/pg/StartPay/A-first')
|
||||
@@ -532,7 +539,7 @@ class PaymentRedirectViewTests(TestCase):
|
||||
self.order.gateway_authority = 'A-existing'
|
||||
self.order.save(update_fields=['gateway_authority'])
|
||||
|
||||
response = self.client.get(f'/api/payment/orders/{self.order.id}/pay')
|
||||
response = self.client.get(_pay_path(self.order))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, 'https://www.zarinpal.com/pg/StartPay/A-existing')
|
||||
@@ -543,13 +550,35 @@ class PaymentRedirectViewTests(TestCase):
|
||||
self.order.status = 'paid'
|
||||
self.order.save(update_fields=['status'])
|
||||
|
||||
response = self.client.get(f'/api/payment/orders/{self.order.id}/pay')
|
||||
response = self.client.get(_pay_path(self.order))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
mock_request_payment.assert_not_called()
|
||||
|
||||
@patch('payment.views.zarinpal.request_payment')
|
||||
def test_link_without_or_with_foreign_signature_is_404(self, mock_request_payment):
|
||||
# شناسهی سفارش ترتیبی است؛ غریبه نباید با پیمایشِ /orders/<n>/pay روی سفارشِ دیگران
|
||||
# authority بسازد یا وضعیتش را از ریدایرکت بخواند.
|
||||
other = Order.objects.create(
|
||||
user_id=2, amount=100000, currency='IRT', provider='gateway', idempotency_key='pay-view-other',
|
||||
)
|
||||
for path in (
|
||||
f'/api/payment/orders/{self.order.id}/pay',
|
||||
f'/api/payment/orders/{self.order.id}/pay?t={pay_link_token(other.id)}',
|
||||
f'/api/payment/orders/{self.order.id}/pay?t=garbage',
|
||||
):
|
||||
self.assertEqual(self.client.get(path).status_code, 404, path)
|
||||
mock_request_payment.assert_not_called()
|
||||
self.order.refresh_from_db()
|
||||
self.assertEqual(self.order.gateway_authority, '')
|
||||
|
||||
def test_expired_link_is_404(self):
|
||||
path = _pay_path(self.order)
|
||||
with patch('django.core.signing.time.time', return_value=time.time() + PAY_LINK_MAX_AGE_SECONDS + 5):
|
||||
self.assertEqual(self.client.get(path).status_code, 404)
|
||||
|
||||
def test_unknown_order_is_404(self):
|
||||
response = self.client.get('/api/payment/orders/999999/pay')
|
||||
response = self.client.get(f'/api/payment/orders/999999/pay?t={pay_link_token(999999)}')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_bazaar_order_is_404_on_gateway_pay_endpoint(self):
|
||||
@@ -557,7 +586,7 @@ class PaymentRedirectViewTests(TestCase):
|
||||
user_id=1, amount=100000, provider='bazaar',
|
||||
bazaar_product_sku='sku', idempotency_key='pay-view-bazaar-1',
|
||||
)
|
||||
response = self.client.get(f'/api/payment/orders/{bazaar_order.id}/pay')
|
||||
response = self.client.get(_pay_path(bazaar_order))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@@ -690,7 +719,7 @@ class PaymentReturnsToTheAppTests(TestCase):
|
||||
status='paid', idempotency_key='ret-2',
|
||||
)
|
||||
|
||||
response = self.client.get(f'/api/payment/orders/{order.id}/pay')
|
||||
response = self.client.get(_pay_path(order))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
|
||||
Reference in New Issue
Block a user