send the Bazaar access token without "Bearer" and cap its cache at an hour
Tried against the real Bazaar API on 2026-09-26 after the first live authorization: "Authorization: Bearer <t>" gets 403 "Access token is not valid", the bare token is accepted, even though token_type says Bearer. The refresh grant also returned expires_in=3600000, which the old code would have cached for ~41 days; the TTL is now capped at one hour and 401/403 clear it. 404 invalid_value (wrong package or SKU) is our misconfiguration, so it now raises 503 instead of failing the user's order; only error=not_found means "not purchased", as Bazaar's docs say. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -166,11 +166,21 @@ def complete_authorization(*, code: str, state: str = '') -> None:
|
|||||||
raise BazaarAuthorizationError('این لینک همین الان یک بار استفاده شد.')
|
raise BazaarAuthorizationError('این لینک همین الان یک بار استفاده شد.')
|
||||||
cache.delete(ACCESS_TOKEN_CACHE_KEY)
|
cache.delete(ACCESS_TOKEN_CACHE_KEY)
|
||||||
if data.get('access_token'):
|
if data.get('access_token'):
|
||||||
cache.set(ACCESS_TOKEN_CACHE_KEY, data['access_token'],
|
cache.set(ACCESS_TOKEN_CACHE_KEY, data['access_token'], timeout=_access_token_ttl(data))
|
||||||
timeout=max(int(data.get('expires_in', 3600)) - 60, 60))
|
|
||||||
logger.info('Bazaar API authorized; refresh token stored')
|
logger.info('Bazaar API authorized; refresh token stored')
|
||||||
|
|
||||||
|
|
||||||
|
def _access_token_ttl(data: dict) -> int:
|
||||||
|
# سند ۳۶۰۰ میگوید ولی بازارِ واقعی ۲۰۲۶-۰۹-۲۶ `expires_in: 3600000` داد
|
||||||
|
# (احتمالاً میلیثانیه). کش را به یک ساعت سقف میزنیم تا ۴۱ روز توکنِ
|
||||||
|
# مرده نگه نداریم؛ روی 401/403 هم پاک میشود.
|
||||||
|
try:
|
||||||
|
seconds = int(data.get('expires_in', 3600))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
seconds = 3600
|
||||||
|
return max(min(seconds, 3600) - 60, 60)
|
||||||
|
|
||||||
|
|
||||||
def _get_access_token() -> str:
|
def _get_access_token() -> str:
|
||||||
"""
|
"""
|
||||||
ACCESS_TOKEN رو کش میکنه (توی cache framework جنگو) تا هر verify یک
|
ACCESS_TOKEN رو کش میکنه (توی cache framework جنگو) تا هر verify یک
|
||||||
@@ -214,9 +224,7 @@ def _get_access_token() -> str:
|
|||||||
logger.error('Bazaar access-token response malformed: status=%s', resp.status_code)
|
logger.error('Bazaar access-token response malformed: status=%s', resp.status_code)
|
||||||
raise BazaarAPIError()
|
raise BazaarAPIError()
|
||||||
|
|
||||||
# طبق سند بازار access_token معمولاً ~۱ ساعت اعتباره؛ کمی زودتر منقضی
|
cache.set(ACCESS_TOKEN_CACHE_KEY, token, timeout=_access_token_ttl(data))
|
||||||
# میکنیم تا لبهی زمانی مشکل نسازه.
|
|
||||||
cache.set(ACCESS_TOKEN_CACHE_KEY, token, timeout=int(data.get('expires_in', 3600)) - 60)
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +242,10 @@ def _auth_headers() -> dict:
|
|||||||
"""
|
"""
|
||||||
if settings.BAZAAR_PISHKHAN_API_SECRET:
|
if settings.BAZAAR_PISHKHAN_API_SECRET:
|
||||||
return {'CAFEBAZAAR-PISHKHAN-API-SECRET': settings.BAZAAR_PISHKHAN_API_SECRET}
|
return {'CAFEBAZAAR-PISHKHAN-API-SECRET': settings.BAZAAR_PISHKHAN_API_SECRET}
|
||||||
return {'Authorization': f'Bearer {_get_access_token()}'}
|
# بدونِ «Bearer»: با آن بازار 403 «Access token is not valid» میدهد، با
|
||||||
|
# توکنِ خام قبول میکند (آزمایش روی بازارِ واقعی، ۲۰۲۶-۰۹-۲۶)، هرچند
|
||||||
|
# token_typeِ خودش Bearer است.
|
||||||
|
return {'Authorization': _get_access_token()}
|
||||||
|
|
||||||
|
|
||||||
def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: bool = False) -> BazaarPurchaseResult:
|
def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: bool = False) -> BazaarPurchaseResult:
|
||||||
@@ -247,7 +258,7 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
|
|||||||
تکخرید: api/validate/<pkg>/inapp/<sku>/purchases/<token>/
|
تکخرید: api/validate/<pkg>/inapp/<sku>/purchases/<token>/
|
||||||
اشتراک: api/applications/<pkg>/subscriptions/<sku>/purchases/<token>
|
اشتراک: api/applications/<pkg>/subscriptions/<sku>/purchases/<token>
|
||||||
به گفتهی سند «تنها اگر error برابر not_found باشد خرید انجام نشده»؛ پس
|
به گفتهی سند «تنها اگر error برابر not_found باشد خرید انجام نشده»؛ پس
|
||||||
فقط ۴۰۴ نامعتبر است و بقیهی خطاها (کلیدِ غلط، سقفِ روزانه، قطعی) ۵۰۳ اند
|
فقط همان نامعتبر است و بقیهی خطاها (کلیدِ غلط، سقفِ روزانه، قطعی) ۵۰۳ اند
|
||||||
تا یک خریدِ واقعی بهخاطرِ خطای ما failed نشود.
|
تا یک خریدِ واقعی بهخاطرِ خطای ما failed نشود.
|
||||||
"""
|
"""
|
||||||
if not is_configured():
|
if not is_configured():
|
||||||
@@ -279,13 +290,16 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
if resp.status_code == 404:
|
error = data.get('error') if isinstance(data, dict) else None
|
||||||
return BazaarPurchaseResult(is_valid=False, raw=data if isinstance(data, dict) else {'status_code': 404})
|
if resp.status_code == 404 and error == 'not_found':
|
||||||
|
return BazaarPurchaseResult(is_valid=False, raw=data)
|
||||||
|
# 404 با invalid_value («Package name is invalid» / «Product is not found»)
|
||||||
|
# یعنی تنظیمِ ما غلط است، نه خریدِ کاربر؛ مثلِ بقیهی خطاها ۵۰۳.
|
||||||
if resp.status_code != 200 or not isinstance(data, dict):
|
if resp.status_code != 200 or not isinstance(data, dict):
|
||||||
if resp.status_code == 401:
|
if resp.status_code in (401, 403):
|
||||||
cache.delete(ACCESS_TOKEN_CACHE_KEY)
|
cache.delete(ACCESS_TOKEN_CACHE_KEY)
|
||||||
logger.error('Bazaar verify-purchase failed: status=%s error=%s', resp.status_code,
|
logger.error('Bazaar verify-purchase failed: status=%s error=%s desc=%s', resp.status_code,
|
||||||
data.get('error') if isinstance(data, dict) else None)
|
error, data.get('error_description') if isinstance(data, dict) else None)
|
||||||
raise BazaarAPIError()
|
raise BazaarAPIError()
|
||||||
|
|
||||||
if is_subscription:
|
if is_subscription:
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class BazaarVerifyPurchaseFailClosedTests(TestCase):
|
|||||||
self.assertTrue(result.is_valid)
|
self.assertTrue(result.is_valid)
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
_, get_kwargs = mock_get.call_args
|
_, get_kwargs = mock_get.call_args
|
||||||
self.assertEqual(get_kwargs['headers']['Authorization'], 'Bearer real-access-token')
|
self.assertEqual(get_kwargs['headers']['Authorization'], 'real-access-token')
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
BAZAAR_REFRESH_TOKEN='refresh-token-xyz',
|
BAZAAR_REFRESH_TOKEN='refresh-token-xyz',
|
||||||
|
|||||||
@@ -162,6 +162,18 @@ class BazaarValidateAgainstDocsTests(TestCase):
|
|||||||
self._resp(mock_get, 404, {'error': 'not_found'})
|
self._resp(mock_get, 404, {'error': 'not_found'})
|
||||||
self.assertFalse(bazaar.verify_purchase(product_sku='s', purchase_token='forged').is_valid)
|
self.assertFalse(bazaar.verify_purchase(product_sku='s', purchase_token='forged').is_valid)
|
||||||
|
|
||||||
|
@patch('payment.bazaar.requests.get')
|
||||||
|
def test_wrong_package_or_sku_is_503_not_invalid(self, mock_get):
|
||||||
|
# 404ِ invalid_value یعنی تنظیمِ ما غلط است، نه خریدِ جعلی.
|
||||||
|
self._resp(mock_get, 404, {'error': 'invalid_value', 'error_description': 'Package name is invalid.'})
|
||||||
|
with self.assertRaises(bazaar.BazaarAPIError):
|
||||||
|
bazaar.verify_purchase(product_sku='s', purchase_token='t')
|
||||||
|
|
||||||
|
def test_access_token_ttl_is_capped_at_an_hour(self):
|
||||||
|
# بازارِ واقعی expires_in=3600000 داد.
|
||||||
|
self.assertEqual(bazaar._access_token_ttl({'expires_in': 3600000}), 3540)
|
||||||
|
self.assertEqual(bazaar._access_token_ttl({'expires_in': 3600}), 3540)
|
||||||
|
|
||||||
@patch('payment.bazaar.requests.get')
|
@patch('payment.bazaar.requests.get')
|
||||||
def test_our_credential_error_is_503_not_invalid(self, mock_get):
|
def test_our_credential_error_is_503_not_invalid(self, mock_get):
|
||||||
# کلیدِ غلط یا سقفِ روزانه نباید خریدِ واقعیِ کاربر را failed کند.
|
# کلیدِ غلط یا سقفِ روزانه نباید خریدِ واقعیِ کاربر را failed کند.
|
||||||
|
|||||||
Reference in New Issue
Block a user