accounts.0008 drops the columns and refuses to run while guest rows exist. Docs describe the current state; tests that only proved guest behaviour are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
248 lines
13 KiB
Python
248 lines
13 KiB
Python
"""
|
||
حفاظهای بازبینیِ امنیتیِ پیش از تحویل (پرداخت). هر تست رفتاری را نگه میدارد که پیشتر غلط بود.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from django.contrib.auth.models import Group
|
||
from django.core.cache import cache
|
||
from django.core.management import call_command
|
||
from django.test import TestCase, override_settings
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import User
|
||
from discount.models import DiscountCode
|
||
from subscription.models import Plan
|
||
from subscription.services import credit_balance
|
||
|
||
from .bazaar import BazaarPurchaseResult
|
||
from .models import Order, Transaction
|
||
from .services import apply_order_payment_result
|
||
|
||
|
||
def client_for(user):
|
||
c = APIClient()
|
||
c.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||
return c
|
||
|
||
|
||
class _Base(TestCase):
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120009901')
|
||
self.app = client_for(self.user)
|
||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||
|
||
def tearDown(self):
|
||
cache.clear()
|
||
|
||
def pack_order(self, **extra):
|
||
r = self.app.post('/api/subscription/purchase', {'planId': str(self.pack.id), **extra}, format='json')
|
||
return Order.objects.get(pk=r.json()['orderId'])
|
||
|
||
|
||
class SupervisorCannotRunMoneyActionsTests(_Base):
|
||
"""گروهِ Supervisor فقط view_order دارد؛ actionِ بیpermissions برای هر بینندهی فهرست اجرا میشد."""
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
call_command('setup_supervisor_group', verbosity=0)
|
||
staff = get_user_model().objects.create_user('sup', 'sup@example.com', 'pw-for-tests-only', is_staff=True)
|
||
staff.groups.add(Group.objects.get(name='Supervisor'))
|
||
self.client.force_login(staff)
|
||
|
||
def test_refund_is_refused(self):
|
||
order = self.pack_order()
|
||
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id='R1', new_status='paid', event='t')
|
||
self.client.post('/admin/payment/order/', {'action': 'refund', '_selected_action': [order.pk], 'confirm': 'yes'})
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'paid')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
def test_cancel_is_refused(self):
|
||
order = Order.objects.create(user_id=self.user.id, amount=1000, idempotency_key='sec-c1')
|
||
self.client.post('/admin/payment/order/', {'action': 'cancel_pending', '_selected_action': [order.pk]})
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'pending')
|
||
|
||
|
||
class CallbackWithoutAuthorityTests(_Base):
|
||
def test_missing_authority_is_400_and_touches_no_order(self):
|
||
DiscountCode.objects.create(code='SEC50', type='percent', value=50, max_uses=10)
|
||
order = self.pack_order()
|
||
self.assertEqual(self.app.post('/api/discount/redeem', {'code': 'SEC50', 'orderId': str(order.id)},
|
||
format='json').status_code, 200)
|
||
Order.objects.create(user_id=self.user.id, amount=1000, idempotency_key='sec-b') # دومین سفارشِ بازنشده
|
||
|
||
resp = APIClient().get('/api/payment/zarinpal/callback', {'Status': 'NOK'})
|
||
self.assertEqual(resp.status_code, 400)
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'pending')
|
||
|
||
|
||
class PlanEditedBetweenOrderAndPaymentTests(_Base):
|
||
def test_buyer_gets_what_the_order_sold(self):
|
||
order = self.pack_order()
|
||
Plan.objects.filter(pk=self.pack.pk).update(price=order.amount * 5, question_credits=150, hearts_days=60)
|
||
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id='P1', new_status='paid', event='t')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
self.assertEqual(order.question_credits, 20)
|
||
|
||
|
||
@override_settings(BAZAAR_PISHKHAN_API_SECRET='x', BAZAAR_PACKAGE_NAME='com.example')
|
||
class BazaarSecondTokenOnPaidOrderTests(_Base):
|
||
def setUp(self):
|
||
super().setUp()
|
||
Plan.objects.filter(pk=self.pack.pk).update(bazaar_sku='sec-pack20')
|
||
|
||
def test_second_token_is_409_and_stays_usable(self):
|
||
order = self.pack_order(paymentMethod='bazaar')
|
||
valid = BazaarPurchaseResult(is_valid=True, purchase_state=0)
|
||
with patch('payment.bazaar.verify_purchase', return_value=valid):
|
||
first = self.app.post('/api/payment/bazaar/verify',
|
||
{'orderId': str(order.id), 'purchaseToken': 'TOKEN-1'}, format='json')
|
||
again = self.app.post('/api/payment/bazaar/verify',
|
||
{'orderId': str(order.id), 'purchaseToken': 'TOKEN-1'}, format='json')
|
||
second = self.app.post('/api/payment/bazaar/verify',
|
||
{'orderId': str(order.id), 'purchaseToken': 'TOKEN-2'}, format='json')
|
||
self.assertEqual(first.json()['status'], 'paid')
|
||
self.assertEqual(again.status_code, 200) # همان توکن دوباره: بیاثر و بیخطا
|
||
self.assertEqual(second.status_code, 409)
|
||
self.assertFalse(Transaction.objects.filter(gateway_ref_id='TOKEN-2').exists())
|
||
|
||
r2 = self.app.post('/api/subscription/purchase', {'planId': str(self.pack.id), 'paymentMethod': 'bazaar'},
|
||
format='json', HTTP_IDEMPOTENCY_KEY='sec-new')
|
||
with patch('payment.bazaar.verify_purchase', return_value=valid):
|
||
fresh = self.app.post('/api/payment/bazaar/verify',
|
||
{'orderId': r2.json()['orderId'], 'purchaseToken': 'TOKEN-2'}, format='json')
|
||
self.assertEqual(fresh.json()['status'], 'paid')
|
||
self.assertEqual(credit_balance(self.user.id), 40)
|
||
|
||
|
||
@override_settings(BAZAAR_PISHKHAN_API_SECRET='x', BAZAAR_PACKAGE_NAME='com.example')
|
||
class BazaarRejectedVerifyDoesNotBurnTokenTests(_Base):
|
||
"""ردِ بازار (not_found) توکن را روی تراکنشِ «failed» ثبت میکرد و قیدِ یکتا همان خرید را برای همیشه میسوزاند."""
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
Plan.objects.filter(pk=self.pack.pk).update(bazaar_sku='sec-p20')
|
||
self.big = Plan.objects.filter(kind='pack').exclude(pk=self.pack.pk).order_by('-price').first()
|
||
Plan.objects.filter(pk=self.big.pk).update(bazaar_sku='sec-pbig')
|
||
|
||
def verify(self, order_id, token, result):
|
||
with patch('payment.bazaar.verify_purchase', return_value=result):
|
||
return self.app.post('/api/payment/bazaar/verify', {'orderId': str(order_id), 'purchaseToken': token},
|
||
format='json')
|
||
|
||
def test_same_order_after_a_rejection(self):
|
||
order = self.pack_order(paymentMethod='bazaar')
|
||
self.verify(order.id, 'TOK-A', BazaarPurchaseResult(is_valid=False, raw={'error': 'not_found'}))
|
||
r = self.verify(order.id, 'TOK-A', BazaarPurchaseResult(is_valid=True, purchase_state=0))
|
||
self.assertEqual(r.json()['status'], 'paid')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
def test_token_sent_with_the_wrong_order_still_pays_the_right_one(self):
|
||
small = self.pack_order(paymentMethod='bazaar')
|
||
big = self.app.post('/api/subscription/purchase', {'planId': str(self.big.id), 'paymentMethod': 'bazaar'},
|
||
format='json').json()['orderId']
|
||
self.verify(big, 'TOK-S', BazaarPurchaseResult(is_valid=False, raw={'error': 'not_found'}))
|
||
r = self.verify(small.id, 'TOK-S', BazaarPurchaseResult(is_valid=True, purchase_state=0))
|
||
self.assertEqual(r.json()['status'], 'paid')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
self.assertEqual(Order.objects.get(pk=big).status, 'failed')
|
||
|
||
def test_refunded_order_refuses_a_new_token(self):
|
||
from .services import refund_order
|
||
|
||
order = self.pack_order(paymentMethod='bazaar')
|
||
self.verify(order.id, 'TOK-1', BazaarPurchaseResult(is_valid=True, purchase_state=0))
|
||
refund_order(Order.objects.get(pk=order.pk))
|
||
r = self.verify(order.id, 'TOK-2', BazaarPurchaseResult(is_valid=True, purchase_state=0))
|
||
self.assertEqual(r.status_code, 409)
|
||
self.assertEqual(Order.objects.get(pk=order.pk).status, 'refunded')
|
||
self.assertFalse(Transaction.objects.filter(gateway_ref_id='TOK-2').exists())
|
||
|
||
|
||
class OrderAdminMoneyFieldsReadOnlyTests(_Base):
|
||
"""اجازهی change (لازمِ لغو/بازپرداخت) فرمِ سفارش را هم باز میکرد: مبلغ و تعدادِ سوالِ سفارش ویرایششدنی بود."""
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
from django.contrib.auth.models import Permission
|
||
|
||
staff = get_user_model().objects.create_user('ops', 'ops@example.com', 'pw-for-tests-only', is_staff=True)
|
||
group = Group.objects.create(name='ops-sec')
|
||
group.permissions.set(Permission.objects.filter(
|
||
content_type__app_label='payment',
|
||
codename__in=['view_order', 'change_order', 'delete_order', 'add_order',
|
||
'view_transaction', 'change_transaction', 'delete_transaction', 'add_transaction']))
|
||
staff.groups.add(group)
|
||
self.client.force_login(staff)
|
||
|
||
def test_amount_and_credits_cannot_be_edited(self):
|
||
order = self.pack_order()
|
||
data = {'amount': 1000, 'question_credits': 100000, 'user_id': order.user_id,
|
||
'transactions-TOTAL_FORMS': 0, 'transactions-INITIAL_FORMS': 0,
|
||
'transactions-MIN_NUM_FORMS': 0, 'transactions-MAX_NUM_FORMS': 1000}
|
||
self.client.post(f'/admin/payment/order/{order.pk}/change/', data)
|
||
order.refresh_from_db()
|
||
self.assertEqual((order.amount, order.question_credits), (self.pack.price, 20))
|
||
page = self.client.get(f'/admin/payment/order/{order.pk}/change/')
|
||
self.assertNotContains(page, 'name="amount"')
|
||
|
||
def test_orders_and_transactions_are_not_deleted_or_added_from_the_panel(self):
|
||
order = self.pack_order()
|
||
apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id='DEL-1', new_status='paid', event='t')
|
||
self.client.post(f'/admin/payment/order/{order.pk}/delete/', {'post': 'yes'})
|
||
txn = Transaction.objects.get(gateway_ref_id='DEL-1')
|
||
self.client.post(f'/admin/payment/transaction/{txn.pk}/delete/', {'post': 'yes'})
|
||
self.assertTrue(Order.objects.filter(pk=order.pk).exists())
|
||
self.assertTrue(Transaction.objects.filter(pk=txn.pk).exists())
|
||
self.assertEqual(self.client.get('/admin/payment/order/add/').status_code, 403)
|
||
self.assertEqual(self.client.get('/admin/payment/transaction/add/').status_code, 403)
|
||
|
||
|
||
class DiscountDuringPaymentRequestTests(_Base):
|
||
"""کدِ تخفیفی که وسطِ PaymentRequest ثبت شود، authority را با مبلغِ قبلی روی سفارشِ ارزانتر مینشاند."""
|
||
|
||
def test_authority_matches_the_final_amount(self):
|
||
from .zarinpal import ZarinpalRequestResult
|
||
|
||
DiscountCode.objects.create(code='SECHALF', type='percent', value=50, max_uses=5)
|
||
r = self.app.post('/api/subscription/purchase', {'planId': str(self.pack.id)}, format='json')
|
||
order_id, pay_url = r.json()['orderId'], r.json()['paymentUrl']
|
||
requested = []
|
||
|
||
def fake_request(order):
|
||
requested.append(order.amount)
|
||
if len(requested) == 1: # کاربر در تبِ دیگر همان لحظه کد را میزند
|
||
self.app.post('/api/discount/redeem', {'code': 'SECHALF', 'orderId': order_id}, format='json')
|
||
return ZarinpalRequestResult(authority=f'A-{len(requested)}', payment_url='x')
|
||
|
||
with patch('payment.zarinpal.request_payment', side_effect=fake_request):
|
||
resp = APIClient().get('/api/' + pay_url.split('/api/', 1)[1])
|
||
order = Order.objects.get(pk=order_id)
|
||
self.assertEqual(resp.status_code, 302)
|
||
self.assertEqual(order.amount, self.pack.price // 2)
|
||
self.assertEqual(requested, [self.pack.price, self.pack.price // 2])
|
||
self.assertEqual(order.gateway_authority, 'A-2')
|
||
|
||
|
||
class BazaarTokenNotGuessableByAdminFilterTests(TestCase):
|
||
"""فیلترِ querystring روی refresh_token/pending_state توکنِ بازار را حرفبهحرف لو نمیدهد."""
|
||
|
||
def test_filter_is_refused(self):
|
||
from django.contrib.auth import get_user_model
|
||
from django.contrib.auth.models import Permission
|
||
|
||
from .models import BazaarCredential
|
||
BazaarCredential.objects.update_or_create(pk=1, defaults={'refresh_token': 'TOPSECRET', 'pending_state': 'st8'})
|
||
staff = get_user_model().objects.create_user('bz', 'bz@example.com', 'pw-for-tests-only', is_staff=True)
|
||
staff.user_permissions.add(Permission.objects.get(content_type__app_label='payment', codename='view_bazaarcredential'))
|
||
self.client.force_login(staff)
|
||
self.assertEqual(self.client.get('/admin/payment/bazaarcredential/').status_code, 200)
|
||
for lookup in ('refresh_token__startswith', 'pending_state__startswith', 'refresh_token__regex'):
|
||
r = self.client.get('/admin/payment/bazaarcredential/', {lookup: 'T'})
|
||
self.assertEqual(r.status_code, 400, lookup)
|