""" حفاظ‌های بازبینیِ امنیتیِ پیش از تحویل (پرداخت). هر تست رفتاری را نگه می‌دارد که پیش‌تر غلط بود. """ 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)