159 lines
7.4 KiB
Python
159 lines
7.4 KiB
Python
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)
|