import importlib import os from unittest.mock import patch from django.core.cache import cache from django.test import TestCase, override_settings from rest_framework.test import APIRequestFactory from . import bazaar from .permissions import HasInternalServiceKey, HasWebhookSecret class SecretDefaultsFailClosedTests(TestCase): """ تست‌های متمرکز روی این فیکس: INTERNAL_SERVICE_KEY و PAYMENT_WEBHOOK_SECRET دیگه در پروداکشن (DEBUG=False) دیفالتِ قابل‌حدس‌زدن ندارن. اگه env تنظیم نشده باشه، مقدار خالی می‌مونه و پرمیشن‌های مربوطه صراحتاً رد می‌کنن (fail closed)، نه اینکه با یک مقدارِ شناخته‌شده قبول کنن. """ def setUp(self): self.factory = APIRequestFactory() @override_settings(DEBUG=False, INTERNAL_SERVICE_KEY='') def test_production_missing_internal_key_rejects(self): request = self.factory.post('/api/payment/orders') # حتی اگه کلاینت هیچ هدری هم نفرسته یا مقدارِ خالی بفرسته، چون # settings.INTERNAL_SERVICE_KEY خودش خالیه، درخواست باید رد بشه. self.assertFalse(HasInternalServiceKey().has_permission(request, None)) @override_settings(DEBUG=False, PAYMENT_WEBHOOK_SECRET='') def test_production_missing_webhook_secret_rejects(self): request = self.factory.post('/api/payment/webhook') self.assertFalse(HasWebhookSecret().has_permission(request, None)) @override_settings( DEBUG=False, INTERNAL_SERVICE_KEY='prod-internal-key-xyz', PAYMENT_WEBHOOK_SECRET='prod-webhook-secret-xyz', ) def test_production_configured_secrets_accepts(self): internal_request = self.factory.post( '/api/payment/orders', HTTP_X_INTERNAL_KEY='prod-internal-key-xyz', ) self.assertTrue(HasInternalServiceKey().has_permission(internal_request, None)) webhook_request = self.factory.post( '/api/payment/webhook', HTTP_X_WEBHOOK_SECRET='prod-webhook-secret-xyz', ) self.assertTrue(HasWebhookSecret().has_permission(webhook_request, None)) def test_debug_mode_local_fallback_still_valid(self): # settings.py این مقادیر رو یک‌بار موقعِ import، بر اساسِ DEBUG واقعیِ # همون پروسه محاسبه می‌کنه (نه چیزی که override_settings در تست # می‌تونه بعداً toggle کنه). برای تستِ صریحِ همین منطق، ماژولِ # config.settings رو با DJANGO_DEBUG=1 و بدونِ ست‌بودنِ # INTERNAL_SERVICE_KEY/PAYMENT_WEBHOOK_SECRET دوباره reload می‌کنیم # و مطمئن می‌شیم دیفالتِ محلیِ قبلیِ dev هنوز برقراره. from config import settings as settings_module env_overrides = {'DJANGO_DEBUG': '1'} env_removed = {'INTERNAL_SERVICE_KEY', 'PAYMENT_WEBHOOK_SECRET'} clean_env = { k: v for k, v in os.environ.items() if k not in env_removed } clean_env.update(env_overrides) try: with patch.dict(os.environ, clean_env, clear=True): importlib.reload(settings_module) self.assertTrue(settings_module.DEBUG) self.assertEqual( settings_module.INTERNAL_SERVICE_KEY, 'dev-internal-service-key-change-me', ) self.assertEqual( settings_module.PAYMENT_WEBHOOK_SECRET, 'dev-webhook-secret-change-me', ) finally: # همیشه ماژول رو به حالتِ واقعیِ محیطِ تست برمی‌گردونیم تا روی # بقیه‌ی تست‌ها اثر نذاره. importlib.reload(settings_module) class BazaarVerifyPurchaseFailClosedTests(TestCase): """ تست‌های متمرکز روی فیکسِ باگ fail-open در bazaar.verify_purchase: قبلاً وقتی BAZAAR_REFRESH_TOKEN (کلید سرور-به-سرورِ بازار) ست نشده بود، هر purchaseTokenِ غیرخالیِ ارسالی از کلاینت valid فرض می‌شد. حالا باید در این حالت همیشه invalid برگرده، و مسیر واقعیِ صحت‌سنجی با بازار (وقتی کلیدها ست شدن) دست‌نخورده کار کنه. """ def setUp(self): # کش access_token رو بین تست‌ها پاک می‌کنیم تا یک تست روی تست بعدی # اثر نذاره. cache.clear() @override_settings(BAZAAR_REFRESH_TOKEN='') def test_missing_credentials_never_trusts_client_token(self): # حتی با یک purchaseToken دلخواهِ غیرخالی، بدون کلیدهای بازار نباید # valid برگرده. result = bazaar.verify_purchase( product_sku='some_sku', purchase_token='arbitrary-client-supplied-token', ) self.assertFalse(result.is_valid) @override_settings( BAZAAR_REFRESH_TOKEN='refresh-token-xyz', BAZAAR_CLIENT_ID='client-id', BAZAAR_CLIENT_SECRET='client-secret', BAZAAR_PACKAGE_NAME='ir.tadrisino.app', BAZAAR_API_BASE='https://pardakht.cafebazaar.ir/devapi/v2', ) @patch('payment.bazaar.requests.get') @patch('payment.bazaar.requests.post') def test_configured_credentials_and_successful_response_is_valid(self, mock_post, mock_get): mock_post.return_value.raise_for_status = lambda: None mock_post.return_value.json.return_value = { 'access_token': 'real-access-token', 'expires_in': 3600, } mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { 'purchaseState': 0, 'consumptionState': 0, 'developerPayload': '', } result = bazaar.verify_purchase(product_sku='sku1', purchase_token='real-token') self.assertTrue(result.is_valid) mock_get.assert_called_once() _, get_kwargs = mock_get.call_args self.assertEqual(get_kwargs['headers']['Authorization'], 'Bearer real-access-token') @override_settings( BAZAAR_REFRESH_TOKEN='refresh-token-xyz', BAZAAR_CLIENT_ID='client-id', BAZAAR_CLIENT_SECRET='client-secret', BAZAAR_PACKAGE_NAME='ir.tadrisino.app', BAZAAR_API_BASE='https://pardakht.cafebazaar.ir/devapi/v2', ) @patch('payment.bazaar.requests.get') @patch('payment.bazaar.requests.post') def test_configured_credentials_provider_reports_invalid(self, mock_post, mock_get): mock_post.return_value.raise_for_status = lambda: None mock_post.return_value.json.return_value = { 'access_token': 'real-access-token', 'expires_in': 3600, } mock_get.return_value.status_code = 200 # purchaseState=1 یعنی لغوشده (طبق سند بازار)، یعنی خودِ سرور بازار # می‌گه این خرید معتبر نیست. mock_get.return_value.json.return_value = { 'purchaseState': 1, 'consumptionState': 0, 'developerPayload': '', } result = bazaar.verify_purchase(product_sku='sku1', purchase_token='cancelled-token') self.assertFalse(result.is_valid)