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:
2026-10-04 21:12:28 +03:30
parent b3f624a512
commit 645baf846b
22 changed files with 584 additions and 193 deletions

View File

@@ -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(