From 5cbdd002e4eb1adb0f5055e0ef1bf3374c6132d4 Mon Sep 17 00:00:00 2001 From: dotpot Date: Tue, 7 Feb 2017 13:21:59 +0200 Subject: [PATCH] added ability to auto retry on wrong environment request. --- inapppy/appstore.py | 42 ++++++++++++++++++++++++++++++++++-------- setup.py | 2 +- tests/test_appstore.py | 26 ++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/inapppy/appstore.py b/inapppy/appstore.py index 84bbc2e..96599c1 100644 --- a/inapppy/appstore.py +++ b/inapppy/appstore.py @@ -3,7 +3,8 @@ from requests.exceptions import RequestException import requests - +# https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html +# `Table 2-1 Status codes` api_result_ok = 0 api_result_errors = { 21000: InAppPyValidationError('Bad json'), @@ -12,6 +13,8 @@ 21004: InAppPyValidationError('Shared secret mismatch'), 21005: InAppPyValidationError('Server is unavailable'), 21006: InAppPyValidationError('Subscription has expired'), + + # two following errors can use auto_retry_wrong_env_request. 21007: InAppPyValidationError('Sandbox receipt was sent to the production env'), 21008: InAppPyValidationError('Production receipt was sent to the sandbox env'), } @@ -21,14 +24,23 @@ class AppStoreValidator(object): bundle_id = None sandbox = None url = None + auto_retry_wrong_env_request = False - def __init__(self, bundle_id, sandbox=False): + def __init__(self, bundle_id, sandbox=False, auto_retry_wrong_env_request=False): + """ Constructor for AppStoreValidator + + :param bundle_id: apple bundle id + :param sandbox: sandbox mode ? + :param auto_retry_wrong_env_request: auto retry on wrong env ? + """ self.bundle_id = bundle_id self.sandbox = sandbox if not self.bundle_id: raise InAppPyValidationError('`bundle_id` cannot be empty') + self.auto_retry_wrong_env_request = auto_retry_wrong_env_request + self._change_url_by_sandbox() def _change_url_by_sandbox(self): @@ -38,21 +50,35 @@ def _change_url_by_sandbox(self): def post_json(self, request_json): self._change_url_by_sandbox() - return requests.post(self.url, json=request_json).json() + try: + return requests.post(self.url, json=request_json).json() + except (ValueError, RequestException): + raise InAppPyValidationError('HTTP error') def validate(self, receipt, shared_secret=None): + """ Validates receipt against apple services. + + :param receipt: receipt + :param shared_secret: optional shared secret. + :return: validation result or exception. + """ receipt_json = {'receipt-data': receipt} - # if shared secret is provided, attach it as `password`. if shared_secret: receipt_json['password'] = shared_secret - try: + # Do a request. + api_response = self.post_json(receipt_json) + status = api_response['status'] + + # Check retry case. + if self.auto_retry_wrong_env_request and status in [21007, 21008]: + # switch environment + self.sandbox = not self.sandbox + api_response = self.post_json(receipt_json) - except (ValueError, RequestException): - raise InAppPyValidationError('HTTP error') + status = api_response['status'] - status = api_response['status'] if status != api_result_ok: error = api_result_errors.get(status, InAppPyValidationError('Unknown API status')) error.raw_response = api_response diff --git a/setup.py b/setup.py index 8fab1fd..df0ca90 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='inapppy', - version='0.4', + version='0.5', packages=['inapppy'], install_requires=['rsa', 'requests'], description="In-app purchase validation library for Apple AppStore and GooglePlay.", diff --git a/tests/test_appstore.py b/tests/test_appstore.py index b75ef19..4605e52 100644 --- a/tests/test_appstore.py +++ b/tests/test_appstore.py @@ -70,3 +70,29 @@ def test_appstore_validate_attach_raw_response_to_the_exception(): assert mock_method.call_args[0][0] == {'receipt-data': 'test-receipt', 'password': 'shared-secret'} assert ex.raw_response is not None assert ex.raw_response == raw_response + + +def test_appstore_auto_retry_wrong_env_request(): + validator = AppStoreValidator(bundle_id='test-bundle-id', sandbox=False, auto_retry_wrong_env_request=True) + assert validator is not None + assert not validator.sandbox + assert validator.auto_retry_wrong_env_request + + raw_response = {'status': 21007, 'foo': 'bar'} + with pytest.raises(InAppPyValidationError): + with patch.object(AppStoreValidator, 'post_json', return_value=raw_response) as mock_method: + validator.validate(receipt='test-receipt', shared_secret='shared-secret') + assert mock_method.call_count == 1 + assert validator.url == 'https://buy.itunes.apple.com/verifyReceipt' + assert mock_method.call_args[0][0] == {'receipt-data': 'test-receipt', 'password': 'shared-secret'} + assert validator.sandbox is True + + raw_response = {'status': 21008, 'foo': 'bar'} + with pytest.raises(InAppPyValidationError): + with patch.object(AppStoreValidator, 'post_json', return_value=raw_response) as mock_method: + validator.validate(receipt='test-receipt', shared_secret='shared-secret') + assert validator.sandbox is False + assert mock_method.call_count == 1 + assert validator.url == 'https://buy.itunes.apple.com/verifyReceipt' + assert mock_method.call_args[0][0] == {'receipt-data': 'test-receipt', 'password': 'shared-secret'} +