├── tests ├── __init__.py ├── aiohttp_test.py ├── deprecated_test.py ├── aiohttp_test_py35.py ├── itunesiap_test.py ├── legacy_test.py ├── receipt_test.py └── conftest.py ├── MANIFEST.in ├── pyproject.toml ├── setup.py ├── .gitignore ├── tox.ini ├── docs ├── environment.rst ├── request.rst ├── index.rst ├── receipt.rst ├── quick.rst └── conf.py ├── codecov.yml ├── itunesiap ├── __init__.py ├── tools.py ├── exceptions.py ├── request.py ├── verify_aiohttp.py ├── shortcut.py ├── verify_requests.py ├── environment.py ├── legacy.py └── receipt.py ├── .travis.yml ├── setup.cfg ├── LICENSE └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 39.2.0", 4 | ] 5 | build-backend = "setuptools.build_meta" 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | assert tuple(map(int, setuptools.__version__.split('.'))) >= (39, 2, 0), 'Plesase upgrade setuptools by `pip install -U setuptools`' 4 | 5 | setuptools.setup() 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | .env 4 | .cache 5 | *.pyc 6 | *.egg-info 7 | /build 8 | MANIFEST 9 | 10 | .python-version 11 | .idea 12 | .tox 13 | .ropeproject 14 | .coveralls.yml 15 | .coverage 16 | .DS_Store 17 | htmlcov -------------------------------------------------------------------------------- /tests/aiohttp_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[:2] >= (3, 5): 4 | from .aiohttp_test_py35 import * # noqa 5 | else: 6 | import pytest 7 | 8 | @pytest.mark.skip 9 | def test_no_aiohttp_supported_version(): 10 | pass 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,py36,py37,pypy 3 | [testenv] 4 | deps= 5 | pytest 6 | pytest-cov 7 | pytest-lazy-fixture 8 | mock 9 | patch 10 | passenv=* 11 | setenv = 12 | PYTHONDONTWRITEBYTECODE=1 13 | commands= 14 | pip install -e .[test] 15 | py.test --verbose {posargs} -------------------------------------------------------------------------------- /docs/environment.rst: -------------------------------------------------------------------------------- 1 | Environment 2 | =========== 3 | 4 | .. automodule:: itunesiap.environment 5 | 6 | 7 | .. autoclass:: itunesiap.environment.Environment 8 | :members: 9 | 10 | .. autodata:: itunesiap.environment.default 11 | .. autodata:: itunesiap.environment.production 12 | .. autodata:: itunesiap.environment.sandbox 13 | .. autodata:: itunesiap.environment.review 14 | .. autodata:: itunesiap.environment.unsafe 15 | -------------------------------------------------------------------------------- /docs/request.rst: -------------------------------------------------------------------------------- 1 | Request 2 | ======= 3 | 4 | .. automodule:: itunesiap.request 5 | 6 | .. autoclass:: itunesiap.request.Request 7 | :members: 8 | :inherited-members: 9 | 10 | .. automodule:: itunesiap.exceptions 11 | 12 | .. autoexception:: itunesiap.exceptions.ItunesServerNotAvailable 13 | .. autoexception:: itunesiap.exceptions.ItunesServerNotReachable 14 | .. autoexception:: itunesiap.exceptions.InvalidReceipt 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | bot: "youknowone" 4 | 5 | coverage: 6 | precision: 2 7 | round: nearest 8 | range: "10...90" 9 | 10 | notify: 11 | slack: 12 | default: 13 | url: 'https://hooks.slack.com/services/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' 14 | attachment: "sunburst, diff" 15 | 16 | status: 17 | project: true 18 | patch: true 19 | changes: true 20 | 21 | comment: 22 | layout: "header, diff, changes, sunburst, uncovered" 23 | behavior: default 24 | -------------------------------------------------------------------------------- /itunesiap/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | itunes-iap 3 | ~~~~~~~~~~ 4 | 5 | Itunes In-app Purchase verification api. 6 | 7 | :copyright: (c) 2013 Jeong YunWon 8 | :license: 2-clause BSD. 9 | """ 10 | 11 | from .request import Request 12 | from .receipt import Response, Receipt, InApp 13 | from .shortcut import verify, aioverify 14 | 15 | from . import exceptions 16 | from . import environment 17 | 18 | exc = exceptions 19 | env = environment # env.default, env.sandbox, env.review 20 | 21 | 22 | __version__ = '2.6.1' 23 | __all__ = ( 24 | '__version__', 'Request', 'Response', 'Receipt', 'InApp', 25 | 'verify', 'aioverify', 26 | 'exceptions', 'exc', 'environment', 'env') 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | sudo: false 4 | python: 5 | - "pypy2.7-7.1.1" 6 | - "pypy3.6-7.1.1" 7 | - "2.7" 8 | - "3.8" 9 | - "3.7" 10 | - "3.6" 11 | - "3.5" 12 | - "3.4" 13 | - "nightly" 14 | # command to install dependencies 15 | install: 16 | - "pip install --upgrade pip" 17 | - "pip install flake8 sphinx '.[test]'" 18 | # command to run test 19 | script: 20 | - "flake8 --ignore=E501,E999 ." 21 | - "pytest --cov=itunesiap -vv tests/" 22 | - "python -msphinx -M html docs build" 23 | after_success: 24 | - bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" 25 | matrix: 26 | allow_failures: 27 | - python: "nightly" 28 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. itunes-iap documentation master file, created by 2 | sphinx-quickstart on Sat Jul 22 17:34:06 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | iTunes In-App Purchase 7 | ====================== 8 | 9 | *itunes-iap* is an easy way to verify iTunes In-App Purchase receipts. 10 | It also is a powerful and flexible interface for them too. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | quick.rst 17 | request.rst 18 | receipt.rst 19 | environment.rst 20 | 21 | .. include:: ../README.rst 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /docs/receipt.rst: -------------------------------------------------------------------------------- 1 | Receipt 2 | ======= 3 | 4 | .. automodule:: itunesiap.receipt 5 | 6 | .. autoclass:: itunesiap.receipt.ObjectMapper 7 | :members: 8 | 9 | .. autoclass:: itunesiap.receipt.Response 10 | :members: __OPAQUE_FIELDS__, __FIELD_ADAPTERS__, __DOCUMENTED_FIELDS__, __UNDOCUMENTED_FIELDS__ 11 | :special-members: 12 | :undoc-members: 13 | 14 | .. autoclass:: itunesiap.receipt.Receipt 15 | :members: __OPAQUE_FIELDS__, __FIELD_ADAPTERS__, __DOCUMENTED_FIELDS__, __UNDOCUMENTED_FIELDS__ 16 | :special-members: 17 | :undoc-members: 18 | 19 | .. autoclass:: itunesiap.receipt.InApp 20 | :members: __OPAQUE_FIELDS__, __FIELD_ADAPTERS__, __DOCUMENTED_FIELDS__, __UNDOCUMENTED_FIELDS__ 21 | :special-members: 22 | :undoc-members: 23 | 24 | -------------------------------------------------------------------------------- /itunesiap/tools.py: -------------------------------------------------------------------------------- 1 | 2 | import warnings 3 | import functools 4 | 5 | 6 | class lazy_property(object): 7 | """http://stackoverflow.com/questions/3012421/python-lazy-property-decorator 8 | """ 9 | 10 | def __init__(self, function): 11 | self.function = function 12 | 13 | def __get__(self, obj, cls): 14 | value = self.function(obj) 15 | setattr(obj, self.function.__name__, value) 16 | return value 17 | 18 | 19 | def deprecated(func): 20 | """https://wiki.python.org/moin/PythonDecoratorLibrary#Generating_Deprecation_Warnings 21 | 22 | This is a decorator which can be used to mark functions 23 | as deprecated. It will result in a warning being emitted 24 | when the function is used.""" 25 | 26 | @functools.wraps(func) 27 | def new_func(*args, **kwargs): 28 | warnings.warn_explicit( 29 | "Call to deprecated function {0}.".format(func.__name__), 30 | category=UserWarning, 31 | filename=func.__code__.co_filename, 32 | lineno=func.__code__.co_firstlineno + 1 33 | ) 34 | return func(*args, **kwargs) 35 | return new_func 36 | -------------------------------------------------------------------------------- /tests/deprecated_test.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Test for deprecated methods to ensure they are not broken. 4 | """ 5 | 6 | import itunesiap 7 | 8 | 9 | def test_context(raw_receipt_legacy): 10 | """Test sandbox receipts with real itunes server.""" 11 | sandbox_receipt = raw_receipt_legacy 12 | request = itunesiap.Request(sandbox_receipt) 13 | 14 | prev_env = itunesiap.env.current() 15 | assert prev_env != itunesiap.env.production 16 | with itunesiap.env.production: 17 | try: 18 | request.verify() 19 | assert False 20 | except itunesiap.exc.InvalidReceipt as e: 21 | assert e.status == 21007 22 | with itunesiap.env.review: 23 | request.verify() 24 | try: 25 | request.verify() 26 | assert False 27 | except itunesiap.exc.InvalidReceipt as e: 28 | assert e.status == 21007 29 | 30 | assert prev_env == itunesiap.env.current() 31 | 32 | with itunesiap.env.review.clone(use_sandbox=False) as env: 33 | assert itunesiap.env.current() == env 34 | assert env.use_production is True 35 | assert env.use_sandbox is False 36 | 37 | assert prev_env == itunesiap.env.current() 38 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = itunes-iap 3 | version = 2.6.1 4 | url = https://github.com/youknowone/itunes-iap 5 | author = Jeong YunWon 6 | author_email = itunesiap@youknowone.org 7 | classifier = 8 | License :: OSI Approved :: BSD License 9 | Programming Language :: Python :: 2 10 | Programming Language :: Python :: 2.7 11 | Programming Language :: Python :: 3 12 | Programming Language :: Python :: 3.4 13 | Programming Language :: Python :: 3.5 14 | Programming Language :: Python :: 3.6 15 | Programming Language :: Python :: 3.7 16 | license = BSD 2-Clause License 17 | license_file = LICENSE 18 | description = 'Apple iTunes In-app purchase verification api.' 19 | long_description = file: README.rst 20 | keywords = itunes,iap,in-app-purchase,apple,in app purchase,asyncio 21 | [options] 22 | packages = itunesiap 23 | install_requires= 24 | requests>=2.18.4 25 | requests[security]>=2.18.4;python_version<"3.6" 26 | prettyexc>=0.6.0 27 | six>=1.10.0 28 | python-dateutil>=2.6.1 29 | pytz 30 | aiohttp>=3.0.9;python_version>="3.5" 31 | aiodns>=3.0.0;python_version>="3.6" 32 | [options.extras_require] 33 | test = 34 | pytest==5.2.2;python_version>="3.5" 35 | pytest>=4.6.7;python_version<"3.5" 36 | pytest-cov>=2.6.1 37 | pytest-lazy-fixture==0.5.1 38 | tox 39 | mock 40 | patch 41 | attrs==18.2.0 42 | pytest-asyncio;python_version>="3.5" 43 | doc = 44 | sphinx 45 | [tool:pytest] 46 | addopts = --verbose --cov itunesiap 47 | python_files = tests/*test.py 48 | norecursedirs = .git py ci 49 | [flake8] 50 | ignore = E501 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Jeong YunWon 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /itunesiap/exceptions.py: -------------------------------------------------------------------------------- 1 | """:mod:`itunesiap.exceptions`""" 2 | from prettyexc import PrettyException as E 3 | from .receipt import Response 4 | 5 | 6 | class RequestError(E): 7 | pass 8 | 9 | 10 | class ItunesServerNotAvailable(RequestError): 11 | '''iTunes server is not available. No response.''' 12 | 13 | 14 | class ItunesServerNotReachable(ItunesServerNotAvailable): 15 | '''iTunes server is not reachable - including connection timeout.''' 16 | 17 | 18 | class InvalidReceipt(RequestError, Response): 19 | '''A receipt was given by iTunes server but it has error.''' 20 | _descriptions = { 21 | 21000: 'The App Store could not read the JSON object you provided.', 22 | 21002: 'The data in the receipt-data property was malformed.', 23 | 21003: 'The receipt could not be authenticated.', 24 | 21004: 'The shared secret you provided does not match the shared secret on file for your account.', 25 | 21005: 'The receipt server is not currently available.', 26 | 21006: 'This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.', 27 | 21007: 'This receipt is a sandbox receipt, but it was sent to the production service for verification.', 28 | 21008: 'This receipt is a production receipt, but it was sent to the sandbox service for verification.', 29 | } 30 | 31 | def __init__(self, response_data): 32 | Response.__init__(self, response_data) 33 | RequestError.__init__(self, response_data) 34 | 35 | @property 36 | def description(self): 37 | return self._descriptions.get(self.status, None) 38 | -------------------------------------------------------------------------------- /tests/aiohttp_test_py35.py: -------------------------------------------------------------------------------- 1 | 2 | """See official document [#documnet]_ for more information. 3 | 4 | .. [#document] https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1 5 | """ 6 | 7 | import pytest 8 | import itunesiap 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_sandbox_aiorequest(raw_receipt_legacy): 13 | """Test sandbox receipt""" 14 | raw_receipt = raw_receipt_legacy 15 | request = itunesiap.Request(raw_receipt) 16 | try: 17 | response = await request.aioverify() 18 | except itunesiap.exc.InvalidReceipt as e: 19 | assert e.status == 21007 20 | assert e.description == e._descriptions[21007] 21 | else: 22 | assert False, response 23 | response = await request.aioverify(env=itunesiap.env.sandbox) 24 | assert response.status == 0 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_invalid_receipt(): 29 | request = itunesiap.Request('wrong receipt') 30 | 31 | with pytest.raises(itunesiap.exc.InvalidReceipt): 32 | await request.aioverify(env=itunesiap.env.production) 33 | 34 | with pytest.raises(itunesiap.exc.InvalidReceipt): 35 | await request.aioverify(env=itunesiap.env.sandbox) 36 | 37 | 38 | @pytest.mark.skip 39 | @pytest.mark.asyncio 40 | async def test_timeout(): 41 | with pytest.raises(itunesiap.exceptions.ItunesServerNotReachable): 42 | await itunesiap.aioverify( 43 | 'DummyReceipt', timeout=0.000001, env=itunesiap.env.review) 44 | 45 | 46 | def test_shortcut(raw_receipt_legacy): 47 | """Test shortcuts""" 48 | itunesiap.aioverify(raw_receipt_legacy, env=itunesiap.env.sandbox) 49 | -------------------------------------------------------------------------------- /itunesiap/request.py: -------------------------------------------------------------------------------- 1 | """:mod:`itunesiap.request`""" 2 | 3 | from itunesiap.verify_requests import RequestsVerify 4 | 5 | try: 6 | from itunesiap.verify_aiohttp import AiohttpVerify 7 | except (SyntaxError, ImportError, AttributeError): # pragma: no cover 8 | class AiohttpVerify(object): 9 | pass 10 | 11 | 12 | class RequestBase(object): 13 | 14 | PRODUCTION_VALIDATION_URL = "https://buy.itunes.apple.com/verifyReceipt" 15 | SANDBOX_VALIDATION_URL = "https://sandbox.itunes.apple.com/verifyReceipt" 16 | STATUS_SANDBOX_RECEIPT_ERROR = 21007 17 | 18 | def __init__( 19 | self, receipt_data, password=None, exclude_old_transactions=False, 20 | **kwargs): 21 | self.receipt_data = receipt_data 22 | self.password = password 23 | self.exclude_old_transactions = exclude_old_transactions 24 | self.proxy_url = kwargs.pop('proxy_url', None) 25 | if kwargs: # pragma: no cover 26 | raise TypeError( 27 | u"__init__ got unexpected keyword argument {}".format( 28 | ', '.join(kwargs.keys()))) 29 | 30 | def __repr__(self): 31 | return u''.format(self.receipt_data[:20]) 32 | 33 | @property 34 | def request_content(self): 35 | """Instantly built request body for iTunes.""" 36 | request_content = { 37 | 'receipt-data': self.receipt_data, 38 | 'exclude-old-transactions': self.exclude_old_transactions} 39 | if self.password is not None: 40 | request_content['password'] = self.password 41 | return request_content 42 | 43 | 44 | class Request(RequestBase, RequestsVerify, AiohttpVerify): 45 | """Validation request with raw receipt. 46 | 47 | Use `verify` method to try verification and get Receipt or exception. 48 | For detail, see also the Apple document: ``_. 49 | 50 | :param str receipt_data: An iTunes receipt data as Base64 encoded string. 51 | :param str password: Only used for receipts that contain auto-renewable subscriptions. Your app's shared secret (a hexadecimal string). 52 | :param bool exclude_old_transactions: Only used for iOS7 style app receipts that contain auto-renewable or non-renewing subscriptions. If value is true, response includes only the latest renewal transaction for any subscriptions. 53 | :param proxy_url: A proxy url to access the iTunes validation url. 54 | (It is an attribute of :func:`verify` but misplaced here) 55 | """ 56 | -------------------------------------------------------------------------------- /docs/quick.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | Create request to create a request to itunes verify api. 5 | 6 | .. sourcecode:: python 7 | 8 | >>> import itunesiap 9 | >>> try: 10 | >>> response = itunesiap.verify(raw_data) # base64-encoded data 11 | >>> except itunesiap.exc.InvalidReceipt as e: 12 | >>> print('invalid receipt') 13 | >>> print response.receipt.last_in_app.product_id 14 | >>> # other values are also available as properties! 15 | 16 | Practically useful attributes are: `product_id`, `original_transaction_id`, `quantity` and `unique_identifier`. 17 | See the full document in :class:`itunesiap.receipt.InApp`. 18 | 19 | For :mod:`asyncio`, replace :func:`itunesiap.verify` funciton to 20 | :func:`itunesiap.aioverify`. That's all. 21 | 22 | .. sourcecode:: python 23 | 24 | >>> response = itunesiap.aioverify(raw_data) 25 | 26 | 27 | itunesiap.verify() 28 | ------------------ 29 | Note that most of the use cases are covered by the :func:`itunesiap.verify` 30 | function. 31 | 32 | .. autofunction:: itunesiap.verify 33 | 34 | .. autofunction:: itunesiap.aioverify 35 | 36 | 37 | Apple in-review mode 38 | -------------------- 39 | 40 | In review mode, your actual users who use older versions want to verify in 41 | production server but the reviewers in Apple office want to verify in sandbox 42 | server. 43 | 44 | Note: The default env is `production` mode which doesn't allow any sandbox 45 | verifications. 46 | 47 | You can change the verifying mode by specifying `env`. 48 | 49 | .. sourcecode:: python 50 | 51 | >>> # review mode 52 | >>> itunesiap.verify(raw_data, env=itunesiap.env.review) 53 | >>> # sandbox mode 54 | >>> itunesiap.verify(raw_data, env=itunesiap.env.sandbox) 55 | 56 | Also directly passing arguments are accepted: 57 | 58 | .. sourcecode:: python 59 | 60 | >>> # review mode 61 | >>> itunesiap.verify(raw_data, use_production=True, use_sandbox=True) 62 | 63 | 64 | Password for shared secret 65 | -------------------------- 66 | 67 | When you have shared secret for your app, the verifying process requires a 68 | shared secret password. 69 | 70 | About the shared secret, See: In-App_Purchase_Configuration_Guide_. 71 | 72 | .. sourcecode:: python 73 | 74 | >>> try: 75 | >>> # Add password as a parameter 76 | >>> response = itunesiap.verify(raw_data, password=password) 77 | >>> except itunesiap.exc.InvalidReceipt as e: 78 | >>> print('invalid receipt') 79 | >>> in_app = response.receipt.last_in_app # Get the latest receipt returned by Apple 80 | 81 | 82 | .. _In-App_Purchase_Configuration_Guide: https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | itunes-iap v2 2 | ~~~~~~~~~~~~~ 3 | 4 | Python 2 & 3 compatible! Even with :mod:`asyncio` support! 5 | 6 | .. image:: https://travis-ci.org/youknowone/itunes-iap.svg?branch=master 7 | :target: https://travis-ci.org/youknowone/itunes-iap 8 | .. image:: https://coveralls.io/repos/github/youknowone/itunes-iap/badge.svg?branch=master 9 | :target: https://coveralls.io/github/youknowone/itunes-iap?branch=master 10 | 11 | - Source code: ``_ 12 | - Documentation: ``_ 13 | - Distribution: ``_ 14 | 15 | 16 | Quickstart 17 | ---------- 18 | 19 | Create request to create a request to itunes verifying api. 20 | 21 | .. sourcecode:: python 22 | 23 | >>> import itunesiap 24 | >>> try: 25 | >>> response = itunesiap.verify(raw_data) # base64-encoded data 26 | >>> except itunesiap.exc.InvalidReceipt as e: 27 | >>> print('invalid receipt') 28 | >>> print response.receipt.last_in_app.product_id # other values are also available as property! 29 | 30 | The common attributes are: 31 | `product_id`, `original_transaction_id` and `quantity`. 32 | 33 | See the full document in: 34 | - :func:`itunesiap.verify`: The verifying function. 35 | - :class:`itunesiap.receipt.Receipt`: The receipt object. 36 | 37 | 38 | asyncio 39 | ------- 40 | 41 | .. sourcecode:: python 42 | 43 | >>> import itunesiap 44 | >>> response = await itunesiap.aioverify(raw_data) # verify -> aioverify 45 | 46 | The other parts are the same. 47 | 48 | See the full document in: 49 | - :func:`itunesiap.aioverify`: The verifying function. 50 | 51 | 52 | Installation 53 | ------------ 54 | 55 | PyPI is the recommended way. 56 | 57 | .. sourcecode:: shell 58 | 59 | $ pip install itunes-iap 60 | 61 | To browse versions and tarballs, visit: 62 | ``_ 63 | 64 | 65 | Apple in-review mode 66 | -------------------- 67 | 68 | In review mode, your actual users who use older versions want to verify in 69 | production server but the reviewers in Apple office want to verify in sandbox 70 | server. 71 | 72 | Note: The default env is `production` mode which doesn't allow any sandbox 73 | verifications. 74 | 75 | You can change the verifying mode by specifying `env`. 76 | 77 | .. sourcecode:: python 78 | 79 | >>> # review mode 80 | >>> itunesiap.verify(raw_data, env=itunesiap.env.review) 81 | 82 | 83 | Note for v1 users 84 | ----------------- 85 | 86 | There was breaking changes between v1 and v2 APIs. 87 | 88 | - Specify version `0.6.6` for latest v1 API when you don't need new APIs. 89 | - Or use `import itunesiap.legacy as itunesiap` instead of `import itunesiap`. (`from itunesiap import xxx` to `from itunesiap.legacy import xxx`) 90 | 91 | 92 | Contributors 93 | ------------ 94 | 95 | See https://github.com/youknowone/itunes-iap/graphs/contributors 96 | -------------------------------------------------------------------------------- /itunesiap/verify_aiohttp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import aiohttp 4 | 5 | from . import receipt 6 | from . import exceptions 7 | from .environment import default as default_env 8 | 9 | 10 | class AiohttpVerify: 11 | 12 | async def aioverify_from(self, url, timeout): 13 | body = json.dumps(self.request_content).encode() 14 | async with aiohttp.ClientSession() as session: 15 | try: 16 | http_response = await session.post(url, data=body, timeout=timeout) 17 | except asyncio.TimeoutError as e: 18 | raise exceptions.ItunesServerNotReachable(exc=e) 19 | if http_response.status != 200: 20 | response_text = await http_response.text() 21 | raise exceptions.ItunesServerNotAvailable(http_response.status, response_text) 22 | response_body = await http_response.text() 23 | response_data = json.loads(response_body) 24 | response = receipt.Response(response_data) 25 | if response.status != 0: 26 | raise exceptions.InvalidReceipt(response_data) 27 | return response 28 | 29 | async def aioverify(self, **options): 30 | """Try to verify the given receipt with current environment. 31 | 32 | Note that python3.4 support is only available at itunesiap==2.5.1 33 | 34 | See also: 35 | - Receipt_Validation_Programming_Guide_. 36 | 37 | .. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html 38 | 39 | :param itunesiap.environment.Environment env: Override the environment. 40 | :param float timeout: The value is connection timeout of the verifying 41 | request. The default value is 30.0 when no `env` is given. 42 | :param bool use_production: The value is weather verifying in 43 | production server or not. The default value is :class:`bool` True 44 | when no `env` is given. 45 | :param bool use_sandbox: The value is weather verifying in 46 | sandbox server or not. The default value is :class:`bool` False 47 | when no `env` is given. 48 | 49 | :param bool verify_ssl: The value will be ignored. 50 | 51 | :return: :class:`itunesiap.receipt.Receipt` object if succeed. 52 | :raises: Otherwise raise a request exception. 53 | """ 54 | env = options.get('env', default_env) 55 | use_production = options.get('use_production', env.use_production) 56 | use_sandbox = options.get('use_sandbox', env.use_sandbox) 57 | verify_ssl = options.get('verify_ssl', env.verify_ssl) # noqa 58 | timeout = options.get('timeout', env.timeout) 59 | 60 | response = None 61 | if use_production: 62 | try: 63 | response = await self.aioverify_from(self.PRODUCTION_VALIDATION_URL, timeout=timeout) 64 | except exceptions.InvalidReceipt as e: 65 | if not use_sandbox or e.status != self.STATUS_SANDBOX_RECEIPT_ERROR: 66 | raise 67 | if not response and use_sandbox: 68 | try: 69 | response = await self.aioverify_from(self.SANDBOX_VALIDATION_URL, timeout=timeout) 70 | except exceptions.InvalidReceipt: 71 | raise 72 | return response 73 | -------------------------------------------------------------------------------- /itunesiap/shortcut.py: -------------------------------------------------------------------------------- 1 | """:mod:`itunesiap.shortcut`""" 2 | from .request import Request 3 | 4 | 5 | def verify( 6 | receipt_data, password=None, exclude_old_transactions=False, **kwargs): 7 | """Shortcut API for :class:`itunesiap.request.Request`. 8 | 9 | See also: 10 | - Receipt_Validation_Programming_Guide_. 11 | 12 | .. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html 13 | 14 | :param str receipt_data: :class:`itunesiap.request.Request` argument. 15 | An iTunes receipt data as Base64 encoded string. 16 | :param str password: :class:`itunesiap.request.Request` argument. Optional. 17 | Only used for receipts that contain auto-renewable subscriptions. Your 18 | app's shared secret (a hexadecimal string). 19 | :param bool exclude_old_transactions: :class:`itunesiap.request.Request` 20 | argument. Optional. Only used for iOS7 style app receipts that contain 21 | auto-renewable or non-renewing subscriptions. If value is true, 22 | response includes only the latest renewal transaction for any 23 | subscriptions. 24 | 25 | :param itunesiap.environment.Environment env: Set base environment value. 26 | See :mod:`itunesiap.environment` for detail. 27 | :param float timeout: :func:`itunesiap.request.Request.verify` argument. 28 | Keyword-only optional. The value is connection timeout of the verifying 29 | request. The default value is 30.0 when no `env` is given. 30 | :param bool use_production: :func:`itunesiap.request.Request.verify` 31 | argument. Keyword-only optional. The value is weather verifying in 32 | production server or not. The default value is :class:`bool` True 33 | when no `env` is given. 34 | :param bool use_sandbox: :func:`itunesiap.request.Request.verify` 35 | argument. Keyword-only optional. The value is weather verifying in 36 | sandbox server or not. The default value is :class:`bool` False 37 | when no `env` is given. 38 | 39 | :param bool verify_ssl: :func:`itunesiap.request.Request.verify` argument. 40 | Keyword-only optional. The value is weather enabling SSL verification 41 | or not. WARNING: DO NOT TURN IT OFF WITHOUT A PROPER REASON. IF YOU 42 | DON'T UNDERSTAND WHAT IT MEANS, NEVER SET IT YOURSELF. 43 | :param str proxy_url: Keyword-only optional. A proxy url to access the 44 | iTunes validation url. 45 | 46 | :return: :class:`itunesiap.receipt.Receipt` object if succeed. 47 | :raises: Otherwise raise a request exception in :mod:`itunesiap.exceptions`. 48 | """ 49 | proxy_url = kwargs.pop('proxy_url', None) 50 | request = Request( 51 | receipt_data, password, exclude_old_transactions, proxy_url=proxy_url) 52 | return request.verify(**kwargs) 53 | 54 | 55 | def aioverify( 56 | receipt_data, password=None, exclude_old_transactions=False, **kwargs): 57 | """Shortcut API for :class:`itunesiap.request.Request`. 58 | 59 | Note that python3.4 support is only available at itunesiap==2.5.1 60 | 61 | For params and returns, see :func:`itunesiap.verify`. 62 | """ 63 | proxy_url = kwargs.pop('proxy_url', None) 64 | request = Request( 65 | receipt_data, password, exclude_old_transactions, proxy_url=proxy_url) 66 | return request.aioverify(**kwargs) 67 | -------------------------------------------------------------------------------- /itunesiap/verify_requests.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import functools 4 | import requests 5 | 6 | from . import receipt 7 | from . import exceptions 8 | from .environment import Environment 9 | 10 | 11 | class InvalidReceiptResponse(exceptions.InvalidReceipt, receipt.Response): 12 | pass 13 | 14 | 15 | class RequestsVerify(object): 16 | def verify_from(self, url, timeout=None, verify_ssl=True): 17 | """The actual implemention of verification request. 18 | 19 | :func:`verify` calls this method to try to verifying for each servers. 20 | 21 | :param str url: iTunes verification API URL. 22 | :param float timeout: The value is connection timeout of the verifying 23 | request. The default value is 30.0 when no `env` is given. 24 | :param bool verify_ssl: SSL verification. 25 | 26 | :return: :class:`itunesiap.receipt.Receipt` object if succeed. 27 | :raises: Otherwise raise a request exception. 28 | """ 29 | post_body = json.dumps(self.request_content) 30 | requests_post = requests.post 31 | if self.proxy_url: 32 | protocol = self.proxy_url.split('://')[0] 33 | requests_post = functools.partial(requests_post, proxies={protocol: self.proxy_url}) 34 | if timeout is not None: 35 | requests_post = functools.partial(requests_post, timeout=timeout) 36 | try: 37 | http_response = requests_post(url, post_body, verify=verify_ssl) 38 | except requests.exceptions.RequestException as e: 39 | raise exceptions.ItunesServerNotReachable(exc=e) 40 | 41 | if http_response.status_code != 200: 42 | raise exceptions.ItunesServerNotAvailable(http_response.status_code, http_response.content) 43 | 44 | response_data = json.loads(http_response.content.decode('utf-8')) 45 | response = receipt.Response(response_data) 46 | if response.status != 0: 47 | raise exceptions.InvalidReceipt(response_data=response_data) 48 | return response 49 | 50 | def verify(self, **options): 51 | """Try verification with current environment. 52 | 53 | See also: 54 | - Receipt_Validation_Programming_Guide_. 55 | 56 | .. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html 57 | 58 | :param itunesiap.environment.Environment env: Override the environment. 59 | :param float timeout: The value is connection timeout of the verifying 60 | request. The default value is 30.0 when no `env` is given. 61 | :param bool use_production: The value is weather verifying in 62 | production server or not. The default value is :class:`bool` True 63 | when no `env` is given. 64 | :param bool use_sandbox: The value is weather verifying in 65 | sandbox server or not. The default value is :class:`bool` False 66 | when no `env` is given. 67 | 68 | :param bool verify_ssl: The value is weather enabling SSL verification 69 | or not. WARNING: DO NOT TURN IT OFF WITHOUT A PROPER REASON. IF YOU 70 | DON'T UNDERSTAND WHAT IT MEANS, NEVER SET IT YOURSELF. 71 | 72 | :return: :class:`itunesiap.receipt.Receipt` object if succeed. 73 | :raises: Otherwise raise a request exception. 74 | """ 75 | env = options.get('env') 76 | if not env: # backward compitibility 77 | env = Environment._stack[-1] 78 | use_production = options.get('use_production', env.use_production) 79 | use_sandbox = options.get('use_sandbox', env.use_sandbox) 80 | verify_ssl = options.get('verify_ssl', env.verify_ssl) 81 | timeout = options.get('timeout', env.timeout) 82 | assert(env.use_production or env.use_sandbox) 83 | 84 | response = None 85 | if use_production: 86 | try: 87 | response = self.verify_from(self.PRODUCTION_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl) 88 | except exceptions.InvalidReceipt as e: 89 | if not use_sandbox or e.status != self.STATUS_SANDBOX_RECEIPT_ERROR: 90 | raise 91 | 92 | if not response and use_sandbox: 93 | try: 94 | response = self.verify_from(self.SANDBOX_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl) 95 | except exceptions.InvalidReceipt: 96 | raise 97 | 98 | return response 99 | -------------------------------------------------------------------------------- /tests/itunesiap_test.py: -------------------------------------------------------------------------------- 1 | 2 | """See official document [#documnet]_ for more information. 3 | 4 | .. [#document] https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1 5 | """ 6 | 7 | import json 8 | import requests 9 | import itunesiap 10 | 11 | import pytest 12 | 13 | try: 14 | from unittest.mock import patch 15 | except ImportError: 16 | from mock import patch 17 | 18 | 19 | def test_sandbox_request(raw_receipt_legacy): 20 | """Test sandbox receipt""" 21 | raw_receipt = raw_receipt_legacy 22 | request = itunesiap.Request(raw_receipt) 23 | try: 24 | response = request.verify() 25 | assert False 26 | except itunesiap.exc.InvalidReceipt as e: 27 | assert e.status == 21007 28 | assert e.description == e._descriptions[21007] 29 | 30 | request = itunesiap.Request(raw_receipt) 31 | response = request.verify(env=itunesiap.env.sandbox) 32 | assert response.status == 0 33 | 34 | 35 | def test_old_transaction_exclusion(raw_receipt_legacy): 36 | """Test optional old transaction exclusion parameter""" 37 | raw_receipt = raw_receipt_legacy 38 | request = itunesiap.Request(raw_receipt) 39 | response = request.verify(exclude_old_transactions=True, env=itunesiap.env.sandbox) 40 | assert response.status == 0 41 | 42 | 43 | def test_invalid_responses(itunes_response_legacy2): 44 | """Test invalid responses error statuses""" 45 | # We're going to mock the Apple's response and put 21007 status 46 | with patch.object(requests, 'post') as mock_post: 47 | iap_status_21007 = itunes_response_legacy2.copy() 48 | iap_status_21007['status'] = 21007 49 | mock_post.return_value.content = json.dumps(iap_status_21007).encode('utf-8') 50 | mock_post.return_value.status_code = 200 51 | 52 | request = itunesiap.Request('DummyReceipt') 53 | try: 54 | request.verify() 55 | except itunesiap.exc.InvalidReceipt as e: 56 | assert e.status == 21007 57 | assert e.description == e._descriptions[21007] 58 | 59 | 60 | def test_itunes_not_available(): 61 | """Test itunes server errors""" 62 | # We're going to return an invalid http status code 63 | with patch.object(requests, 'post') as mock_post: 64 | mock_post.return_value.content = 'Not avaliable' 65 | mock_post.return_value.status_code = 500 66 | request = itunesiap.Request('DummyReceipt') 67 | try: 68 | request.verify() 69 | except itunesiap.exc.ItunesServerNotAvailable as e: 70 | assert e[0] == 500 71 | assert e[1] == 'Not avaliable' 72 | 73 | 74 | def test_request_fail(): 75 | """Test failure making request to itunes server """ 76 | # We're going to return an invalid http status code 77 | with patch.object(requests, 'post') as mock_post: 78 | mock_post.side_effect = requests.exceptions.ReadTimeout('Timeout') 79 | request = itunesiap.Request('DummyReceipt') 80 | try: 81 | request.verify() 82 | assert False 83 | except itunesiap.exc.RequestError as e: 84 | assert type(e['exc']) == requests.exceptions.ReadTimeout 85 | 86 | 87 | def test_ssl_request_fail(): 88 | """Test failure making request to itunes server """ 89 | # We're going to return an invalid http status code 90 | with patch.object(requests, 'post') as mock_post: 91 | mock_post.side_effect = requests.exceptions.SSLError('Bad ssl') 92 | request = itunesiap.Request('DummyReceipt') 93 | try: 94 | request.verify(verify_request=True) 95 | assert False 96 | except itunesiap.exc.RequestError as e: 97 | assert type(e['exc']) == requests.exceptions.SSLError 98 | 99 | 100 | def test_invalid_receipt(): 101 | request = itunesiap.Request('wrong receipt') 102 | 103 | with pytest.raises(itunesiap.exc.InvalidReceipt): 104 | request.verify(env=itunesiap.env.production) 105 | 106 | with pytest.raises(itunesiap.exc.InvalidReceipt): 107 | request.verify(env=itunesiap.env.sandbox) 108 | 109 | try: 110 | itunesiap.verify('bad data') 111 | except itunesiap.exc.InvalidReceipt as e: 112 | print(e) # __str__ test 113 | print(repr(e)) # __repr__ test 114 | 115 | 116 | def test_timeout(): 117 | with pytest.raises(itunesiap.exceptions.ItunesServerNotReachable): 118 | itunesiap.verify('DummyReceipt', timeout=0.0001) 119 | 120 | 121 | def test_shortcut(raw_receipt_legacy): 122 | """Test shortcuts""" 123 | itunesiap.verify(raw_receipt_legacy, env=itunesiap.env.sandbox) 124 | 125 | 126 | @pytest.mark.parametrize("object", [ 127 | itunesiap.Request('DummyReceipt'), 128 | itunesiap.Response('{}'), 129 | itunesiap.environment.Environment(), 130 | ]) 131 | def test_repr(object): 132 | """Test __repr__""" 133 | '{0!r}'.format(object) 134 | 135 | 136 | if __name__ == '__main__': 137 | pytest.main() 138 | -------------------------------------------------------------------------------- /itunesiap/environment.py: -------------------------------------------------------------------------------- 1 | """:mod:`itunesiap.environment` 2 | 3 | :class:`Environment` is designed to pass pre-defined policies in easy way. 4 | The major use cases are provided as pre-defined constants. 5 | 6 | How to use environments 7 | ----------------------- 8 | 9 | The default policy is `default` and it is the same to `production`. When your 10 | development lifecycle is proceed, you want to change it to `sandbox` or 11 | `review`. 12 | 13 | The recommended way to use environments is passing the value to 14 | :func:`itunesiap.verify` function as keyword argument `env`. 15 | 16 | .. sourcecode:: python 17 | 18 | >>> itunesiap.verify(receipt, env=itunesiap.env.production) 19 | >>> itunesiap.verify(receipt, env=itunesiap.env.sandbox) 20 | >>> itunesiap.verify(receipt, env=itunesiap.env.review) 21 | 22 | 23 | Review mode 24 | ----------- 25 | 26 | This is useful when your server is being used both for real users and Apple 27 | reviewers. 28 | Using review mode for a real service is possible, but be aware of: it is not 29 | 100% safe. Your testers can getting advantage of free IAP in production 30 | version. 31 | A rough solution what I suggest is: 32 | 33 | .. sourcecode:: python 34 | 35 | >>> if client_version == review_version: 36 | >>> env = itunesiap.env.review 37 | >>> else: 38 | >>> env = itunesiap.env.production 39 | >>> 40 | >>> itunesiap.verify(receipt, env=env) 41 | 42 | 43 | Environment 44 | ----------- 45 | """ 46 | from itunesiap.tools import deprecated 47 | 48 | __all__ = ('Environment', 'default', 'production', 'sandbox', 'review') 49 | 50 | 51 | class EnvironmentStack(list): 52 | @deprecated 53 | def push(self, env): 54 | self.append(env) 55 | 56 | 57 | class Environment(object): 58 | """Environment provides option preset for `Request`. `default` is default. 59 | 60 | By passing an environment object to :func:`itunesiap.verify` or 61 | :func:`itunesiap.request.Request.verify` function, it replaces verifying 62 | policies. 63 | """ 64 | 65 | ITEMS = ( 66 | 'use_production', 'use_sandbox', 'timeout', 'exclude_old_transactions', 67 | 'verify_ssl') 68 | 69 | def __init__(self, **kwargs): 70 | self.use_production = kwargs.get('use_production', True) 71 | self.use_sandbox = kwargs.get('use_sandbox', False) 72 | self.timeout = kwargs.get('timeout', None) 73 | self.exclude_old_transactions = kwargs.get('exclude_old_transactions', False) 74 | self.verify_ssl = kwargs.get('verify_ssl', True) 75 | 76 | def __repr__(self): 77 | return u'<{self.__class__.__name__} use_production={self.use_production} use_sandbox={self.use_sandbox} timeout={self.timeout} exclude_old_transactions={self.exclude_old_transactions} verify_ssl={self.verify_ssl}>'.format(self=self) 78 | 79 | def clone(self, **kwargs): 80 | """Clone the environment with additional parameter override""" 81 | options = self.extract() 82 | options.update(**kwargs) 83 | return self.__class__(**options) 84 | 85 | def override(self, **kwargs): 86 | """Override options in kwargs to given object `self`.""" 87 | for item in self.ITEMS: 88 | if item in kwargs: 89 | setattr(self, item, kwargs[item]) 90 | 91 | def extract(self): 92 | """Extract options from `self` and merge to `kwargs` then return a new 93 | dictionary with the values. 94 | """ 95 | options = {} 96 | for item in self.ITEMS: 97 | options[item] = getattr(self, item) 98 | return options 99 | 100 | # backward compatibility features 101 | _stack = EnvironmentStack() 102 | 103 | @deprecated 104 | def push(self): # pragma: no cover 105 | self._stack.push(self) 106 | 107 | @deprecated 108 | def __enter__(self): # pragma: no cover 109 | self._ctx_id = len(self._stack) 110 | self._stack.push(self) 111 | return self 112 | 113 | @deprecated 114 | def __exit__(self, exc_type, exc_value, tb): # pragma: no cover 115 | self._stack.pop(self._ctx_id) 116 | 117 | @classmethod 118 | @deprecated 119 | def current(cls): # pragma: no cover 120 | return cls._stack[-1] 121 | 122 | 123 | #: Use only production server with 30 seconds of timeout. 124 | default = Environment(use_production=True, use_sandbox=False, timeout=30.0, verify_ssl=True) 125 | #: Use only production server with 30 seconds of timeout. 126 | production = Environment(use_production=True, use_sandbox=False, timeout=30.0, verify_ssl=True) 127 | #: Use only sandbox server with 30 seconds of timeout. 128 | sandbox = Environment(use_production=False, use_sandbox=True, timeout=30.0, verify_ssl=True) 129 | 130 | review = Environment(use_production=True, use_sandbox=True, timeout=30.0, verify_ssl=True) 131 | #: Use both production and sandbox servers with 30 seconds of timeout. 132 | 133 | unsafe = Environment(use_production=True, use_sandbox=True, verify_ssl=False) 134 | 135 | 136 | Environment._stack.append(default) # for backward compatibility 137 | 138 | 139 | @deprecated 140 | def current(): 141 | return Environment.current() 142 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def get_version(): 6 | import itunesiap 7 | return itunesiap.__version__ 8 | 9 | # 10 | # itunes-iap documentation build configuration file, created by 11 | # sphinx-quickstart on Sat Jul 22 17:34:06 2017. 12 | # 13 | # This file is execfile()d with the current directory set to its 14 | # containing dir. 15 | # 16 | # Note that not all possible configuration values are present in this 17 | # autogenerated file. 18 | # 19 | # All configuration values have a default; values that are commented out 20 | # serve to show the default. 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | # 26 | # import os 27 | # import sys 28 | # sys.path.insert(0, os.path.abspath('.')) 29 | 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.doctest', 43 | 'sphinx.ext.intersphinx', 44 | 'sphinx.ext.coverage'] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = 'itunes-iap' 60 | copyright = '2017, Jeong YunWon' 61 | author = 'Jeong YunWon' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = get_version() 69 | # The full version, including alpha/beta/rc tags. 70 | release = get_version() 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This patterns also effect to html_static_path and html_extra_path 82 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | 91 | # -- Options for HTML output ---------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | # 96 | html_theme = 'alabaster' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | # 102 | # html_theme_options = {} 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ['_static'] 108 | 109 | # Custom sidebar templates, must be a dictionary that maps document names 110 | # to template names. 111 | # 112 | # This is required for the alabaster theme 113 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 114 | html_sidebars = { 115 | '**': [ 116 | 'about.html', 117 | 'navigation.html', 118 | 'relations.html', # needs 'show_related': True theme option to display 119 | 'searchbox.html', 120 | 'donate.html', 121 | ] 122 | } 123 | 124 | 125 | # -- Options for HTMLHelp output ------------------------------------------ 126 | 127 | # Output file base name for HTML help builder. 128 | htmlhelp_basename = 'itunesiapdoc' 129 | 130 | 131 | # -- Options for LaTeX output --------------------------------------------- 132 | 133 | latex_elements = { 134 | # The paper size ('letterpaper' or 'a4paper'). 135 | # 136 | # 'papersize': 'letterpaper', 137 | 138 | # The font size ('10pt', '11pt' or '12pt'). 139 | # 140 | # 'pointsize': '10pt', 141 | 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | 146 | # Latex figure (float) alignment 147 | # 148 | # 'figure_align': 'htbp', 149 | } 150 | 151 | # Grouping the document tree into LaTeX files. List of tuples 152 | # (source start file, target name, title, 153 | # author, documentclass [howto, manual, or own class]). 154 | latex_documents = [ 155 | (master_doc, 'itunes-iap.tex', 'itunes-iap Documentation', 156 | 'Jeong YunWon', 'manual'), 157 | ] 158 | 159 | 160 | # -- Options for manual page output --------------------------------------- 161 | 162 | # One entry per manual page. List of tuples 163 | # (source start file, name, description, authors, manual section). 164 | man_pages = [ 165 | (master_doc, 'itunes-iap', 'itunes-iap Documentation', 166 | [author], 1) 167 | ] 168 | 169 | 170 | # -- Options for Texinfo output ------------------------------------------- 171 | 172 | # Grouping the document tree into Texinfo files. List of tuples 173 | # (source start file, target name, title, author, 174 | # dir menu entry, description, category) 175 | texinfo_documents = [ 176 | (master_doc, 'itunes-iap', 'itunes-iap Documentation', 177 | author, 'itunes-iap', 'One line description of project.', 178 | 'Miscellaneous'), 179 | ] 180 | 181 | 182 | # Example configuration for intersphinx: refer to the Python standard library. 183 | intersphinx_mapping = {'https://docs.python.org/': None} 184 | -------------------------------------------------------------------------------- /itunesiap/legacy.py: -------------------------------------------------------------------------------- 1 | 2 | from . import exceptions 3 | from .tools import deprecated 4 | import requests 5 | import json 6 | import contextlib 7 | from prettyexc import PrettyException as E 8 | 9 | 10 | RECEIPT_PRODUCTION_VALIDATION_URL = "https://buy.itunes.apple.com/verifyReceipt" 11 | RECEIPT_SANDBOX_VALIDATION_URL = "https://sandbox.itunes.apple.com/verifyReceipt" 12 | 13 | 14 | USE_PRODUCTION = True 15 | USE_SANDBOX = False 16 | 17 | 18 | class ModeNotAvailable(E): 19 | message = '`mode` should be one of `production`, `sandbox`, `review`, `reject`' 20 | 21 | 22 | exceptions.ModeNotAvailable = ModeNotAvailable 23 | 24 | 25 | def config_from_mode(mode): 26 | if mode not in ('production', 'sandbox', 'review', 'reject'): 27 | raise exceptions.ModeNotAvailable(mode) 28 | production = mode in ('production', 'review') 29 | sandbox = mode in ('sandbox', 'review') 30 | return production, sandbox 31 | 32 | 33 | def set_verification_mode(mode): 34 | """Set global verification mode that where allows production or sandbox. 35 | `production`, `sandbox`, `review` or `reject` available. Otherwise raise 36 | an exception. 37 | 38 | `production`: Allows production receipts only. Default. 39 | `sandbox`: Allows sandbox receipts only. 40 | `review`: Allows production receipts but use sandbox as fallback. 41 | `reject`: Reject all receipts. 42 | """ 43 | global USE_PRODUCTION, USE_SANDBOX 44 | USE_PRODUCTION, USE_SANDBOX = config_from_mode(mode) 45 | 46 | 47 | def get_verification_mode(): 48 | if USE_PRODUCTION and USE_SANDBOX: 49 | return 'review' 50 | if USE_PRODUCTION: 51 | return 'production' 52 | if USE_SANDBOX: 53 | return 'sandbox' 54 | return 'reject' 55 | 56 | 57 | class Request(object): 58 | """Validation request with raw receipt. Receipt must be base64 encoded string. 59 | 60 | Use `verify` method to try verification and get Receipt or exception. 61 | """ 62 | 63 | def __init__(self, receipt, password=None, **kwargs): 64 | self.receipt = receipt 65 | self.password = password 66 | self.use_production = kwargs.get('use_production', USE_PRODUCTION) 67 | self.use_sandbox = kwargs.get('use_sandbox', USE_SANDBOX) 68 | self.verify_ssl = kwargs.get('verify_ssl', False) 69 | self.response = None 70 | 71 | def __repr__(self): # pragma: no cover 72 | valid = None 73 | if self.result: 74 | valid = self.result['status'] == 0 75 | return u''.format(valid, self.receipt[:20]) 76 | 77 | @property 78 | def result(self): 79 | return self._extract_receipt(json.loads(self.response.content.decode('utf-8'))) 80 | 81 | @property 82 | def request_content(self): 83 | if self.password is not None: 84 | request_content = {'receipt-data': self.receipt, 'password': self.password} 85 | else: 86 | request_content = {'receipt-data': self.receipt} 87 | return request_content 88 | 89 | def verify_from(self, url, verify_ssl=False): 90 | """Try verification from given url.""" 91 | # If the password exists from kwargs, pass it up with the request, otherwise leave it alone 92 | post_body = json.dumps(self.request_content) 93 | try: 94 | self.response = requests.post(url, post_body, verify=verify_ssl) 95 | except requests.exceptions.RequestException: # pragma: no cover 96 | raise 97 | 98 | if self.response.status_code != 200: 99 | raise exceptions.ItunesServerNotAvailable(self.response.status_code, self.response.content) 100 | 101 | status = self.result['status'] 102 | if status != 0: 103 | e = exceptions.InvalidReceipt(self.result) 104 | e.receipt = self.result.get('receipt', None) 105 | raise e 106 | return self.result 107 | 108 | def _extract_receipt(self, receipt_data): 109 | """There are two formats that iTunes iap purchase receipts are 110 | sent back in 111 | """ 112 | if 'receipt' not in receipt_data: 113 | return receipt_data 114 | in_app_purchase = receipt_data['receipt'].get('in_app', []) 115 | if len(in_app_purchase) > 0: 116 | receipt_data['receipt'].update(in_app_purchase[-1]) 117 | return receipt_data 118 | 119 | @deprecated 120 | def validate(self): # pragma: no cover 121 | return self.verify() 122 | 123 | def verify(self, verify_ssl=None): 124 | """Try verification with settings. Returns a Receipt object if succeeded. 125 | Or raise an exception. See `self.response` or `self.result` to see details. 126 | """ 127 | assert(self.use_production or self.use_sandbox) 128 | if verify_ssl is None: 129 | verify_ssl = self.verify_ssl 130 | if self.use_production: 131 | try: 132 | receipt = self.verify_from(RECEIPT_PRODUCTION_VALIDATION_URL, verify_ssl) 133 | except exceptions.InvalidReceipt: 134 | if not self.use_sandbox: 135 | raise 136 | 137 | if self.use_sandbox: 138 | try: 139 | receipt = self.verify_from(RECEIPT_SANDBOX_VALIDATION_URL, verify_ssl) 140 | except exceptions.InvalidReceipt: # pragma: no cover 141 | raise 142 | 143 | return Receipt(receipt) 144 | 145 | @contextlib.contextmanager 146 | def verification_mode(self, mode): 147 | configs = self.use_production, self.use_sandbox 148 | self.use_production, self.use_sandbox = config_from_mode(mode) 149 | yield 150 | self.use_production, self.use_sandbox = configs 151 | 152 | 153 | class Receipt(object): 154 | """Pretty interface for decoded receipt object. 155 | """ 156 | def __init__(self, data): 157 | self.data = data 158 | self.receipt = data['receipt'] 159 | self.receipt_keys = list(self.receipt.keys()) 160 | 161 | def __repr__(self): # pragma: no cover 162 | return u''.format(self.status, self.receipt) 163 | 164 | @property 165 | def _(self): 166 | return self.data 167 | 168 | @property 169 | def status(self): 170 | return self.data['status'] 171 | 172 | @property 173 | def latest_receipt(self): 174 | return self.data['latest_receipt'] 175 | 176 | def __getattr__(self, key): 177 | if key in self.receipt_keys: 178 | return self.receipt[key] 179 | try: 180 | return super(Receipt, self).__getattr__(key) 181 | except AttributeError: 182 | return super(Receipt, self).__getattribute__(key) 183 | 184 | 185 | def verify(data, test_paid=lambda id: id): 186 | """Convenient verification shortcut. 187 | 188 | :param data: iTunes receipt data 189 | :param test_paid: Function to test the receipt is paid. Function should 190 | raise error to disallow response. Parameter is `original_transaction_id` 191 | :return: :class:`itunesiap.core.Response` 192 | """ 193 | request = Request(data) 194 | response = request.verify() 195 | test_paid(response.original_transaction_id) 196 | return response 197 | -------------------------------------------------------------------------------- /tests/legacy_test.py: -------------------------------------------------------------------------------- 1 | 2 | """See official document [#documnet]_ for more information. 3 | 4 | .. [#document] https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1 5 | """ 6 | import json 7 | from mock import patch 8 | import requests 9 | import unittest 10 | 11 | import itunesiap.legacy as itunesiap 12 | from itunesiap.legacy import Request, Receipt, set_verification_mode 13 | from itunesiap.legacy import exceptions 14 | 15 | from tests.conftest import _raw_receipt_legacy as raw_receipt_legacy 16 | 17 | 18 | class TestsIAP(unittest.TestCase): 19 | 20 | def __init__(self, *args, **kwargs): 21 | super(TestsIAP, self).__init__(*args, **kwargs) 22 | self.iap_response = { 23 | u'status': 0, 24 | u'receipt': { 25 | u'purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 26 | u'product_id': u'TestProduction1', 27 | u'original_transaction_id': u'1000000012345678', 28 | u'unique_identifier': u'bcbdb3d45543920dd9sd5c79a72948001fc22a39', 29 | u'original_purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 30 | u'original_purchase_date': u'2013-01-01 00:00:00 Etc/GMT', 31 | u'bvrs': u'1.0', 32 | u'original_purchase_date_ms': u'1348200000000', 33 | u'purchase_date': u'2013-01-01 00:00:00 Etc/GMT', 34 | u'item_id': u'500000000', 35 | u'purchase_date_ms': u'134820000000', 36 | u'bid': u'org.youknowone.itunesiap', 37 | u'transaction_id': u'1000000012345678', 38 | u'quantity': u'1' 39 | } 40 | } 41 | 42 | # Response with multiple in_app's 43 | self.iap_response_in_app = { 44 | u'status': 0, 45 | u'latest_receipt': u'__RECEIPT_DATA', 46 | u'receipt': { 47 | u'original_purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 48 | u'version_external_identifier': 0, 49 | u'original_purchase_date': u'2013-01-01 07:00:00 Etc/GMT', 50 | u'in_app': [ 51 | { 52 | u'is_trial_period': u'false', 53 | u'purchase_date_pst': u'2013-05-18 20:21:09 America/Los_Angeles', 54 | u'product_id': u'org.itunesiap', 55 | u'original_transaction_id': u'1000000155715958', 56 | u'original_purchase_date_pst': u'2013-05-18 19:29:45 America/Los_Angeles', 57 | u'original_purchase_date': u'2013-05-19 02:29:45 Etc/GMT', 58 | u'original_purchase_date_ms': u'1432002585000', 59 | u'purchase_date': u'2013-05-19 03:21:09 Etc/GMT', 60 | u'purchase_date_ms': u'1432005669000', 61 | u'transaction_id': u'1000000155715958', 62 | u'quantity': u'1' 63 | }, 64 | { 65 | u'is_trial_period': u'false', 66 | u'purchase_date_pst': u'2013-05-19 20:21:09 America/Los_Angeles', 67 | u'product_id': u'org.itunesiap', 68 | u'original_transaction_id': u'1000000155718067', 69 | u'original_purchase_date_pst': u'2013-05-18 19:37:10 America/Los_Angeles', 70 | u'original_purchase_date': u'2013-05-19 02:37:10 Etc/GMT', 71 | u'original_purchase_date_ms': u'1432003030000', 72 | u'purchase_date': u'2013-05-19 03:21:09 Etc/GMT', 73 | u'purchase_date_ms': u'1432005669000', 74 | u'transaction_id': u'1000000155718067', 75 | u'quantity': u'1' 76 | } 77 | ] 78 | } 79 | } 80 | 81 | def test_global_mode(self): 82 | set_verification_mode('production') 83 | assert Request('').use_production is True 84 | assert Request('').use_sandbox is False 85 | set_verification_mode('sandbox') 86 | assert Request('').use_production is False 87 | assert Request('').use_sandbox is True 88 | set_verification_mode('reject') 89 | assert Request('').use_production is False 90 | assert Request('').use_sandbox is False 91 | set_verification_mode('review') 92 | assert Request('').use_production is True 93 | assert Request('').use_sandbox is True 94 | 95 | def test_request(self): 96 | sandbox_receipt = raw_receipt_legacy() 97 | 98 | set_verification_mode('production') 99 | request = Request(sandbox_receipt) 100 | try: 101 | receipt = request.verify() 102 | assert False 103 | except exceptions.InvalidReceipt as e: 104 | assert e.status == 21007 105 | assert e.description == e._descriptions[21007] 106 | set_verification_mode('sandbox') 107 | request = Request(sandbox_receipt) 108 | receipt = request.verify() 109 | assert receipt 110 | 111 | def test_responses(self): 112 | # We're going to mock the Apple's response and put 21007 status 113 | with patch.object(requests, 'post') as mock_post: 114 | iap_status_21007 = self.iap_response.copy() 115 | iap_status_21007['status'] = 21007 116 | mock_post.return_value.content = json.dumps(iap_status_21007).encode('utf-8') 117 | mock_post.return_value.status_code = 200 118 | set_verification_mode('production') 119 | request = Request('DummyReceipt') 120 | try: 121 | request.verify() 122 | except exceptions.InvalidReceipt as e: 123 | assert e.status == 21007 124 | assert e.description == e._descriptions[21007] 125 | 126 | # We're going to return an invalid http status code 127 | with patch.object(requests, 'post') as mock_post: 128 | mock_post.return_value.content = 'Not avaliable' 129 | mock_post.return_value.status_code = 500 130 | set_verification_mode('production') 131 | request = Request('DummyReceipt') 132 | try: 133 | request.verify() 134 | except exceptions.ItunesServerNotAvailable as e: 135 | assert e.args[0] == 500 136 | assert e.args[1] == 'Not avaliable' 137 | 138 | def test_context(self): 139 | sandbox_receipt = raw_receipt_legacy() 140 | request = Request(sandbox_receipt, verify_ssl=True) 141 | configs = request.use_production, request.use_sandbox 142 | with request.verification_mode('production'): 143 | try: 144 | request.verify() 145 | assert False 146 | except exceptions.InvalidReceipt as e: 147 | assert e.status == 21007 148 | with request.verification_mode('review'): 149 | request.verify() 150 | try: 151 | request.verify() 152 | assert False 153 | except exceptions.InvalidReceipt as e: 154 | assert e.status == 21007 155 | assert configs == (request.use_production, request.use_sandbox) 156 | 157 | def test_receipt(self): 158 | receipt = Receipt(self.iap_response) 159 | 160 | assert receipt.status == 0 # 0 is normal 161 | assert receipt.product_id == u'TestProduction1' # 162 | assert receipt.original_transaction_id == u'1000000012345678' # original transaction id 163 | assert receipt.quantity == u'1' # check quantity 164 | assert receipt.unique_identifier == u'bcbdb3d45543920dd9sd5c79a72948001fc22a39' 165 | 166 | def test_receipt2(self): 167 | receipt = Receipt(self.iap_response_in_app) 168 | assert receipt.latest_receipt 169 | 170 | def test_shortcut(self): 171 | sandbox_receipt = raw_receipt_legacy() 172 | mode = itunesiap.get_verification_mode() 173 | itunesiap.set_verification_mode('sandbox') 174 | itunesiap.verify(sandbox_receipt) 175 | itunesiap.set_verification_mode(mode) 176 | 177 | def test_extract_receipt(self): 178 | """ 179 | Testing the extract receipt function. 180 | The function which helps to put the last 'in_app's fields' in the 181 | 'receipt dictionary' 182 | """ 183 | 184 | # Test IAP Response without in_app list 185 | request = Request('DummyReceipt', use_production=True) 186 | ext_receipt = request._extract_receipt(self.iap_response) 187 | 188 | assert ext_receipt['status'] == 0 # 0 is normal 189 | assert ext_receipt['receipt']['product_id'] == u'TestProduction1' 190 | assert ext_receipt['receipt']['original_transaction_id'] == u'1000000012345678' # original transaction id 191 | assert ext_receipt['receipt']['quantity'] == u'1' # check quantity 192 | 193 | # Test IAP Response with in_app list 194 | request = Request('DummyReceipt', use_production=True) 195 | ext_receipt = request._extract_receipt(self.iap_response_in_app) 196 | 197 | assert ext_receipt['status'] == 0 # 0 is normal 198 | assert ext_receipt['receipt']['product_id'] == u'org.itunesiap' 199 | assert ext_receipt['receipt']['original_transaction_id'] == u'1000000155718067' # original transaction id 200 | assert ext_receipt['receipt']['quantity'] == u'1' # check quantity 201 | 202 | 203 | if __name__ == '__main__': 204 | unittest.main() 205 | -------------------------------------------------------------------------------- /tests/receipt_test.py: -------------------------------------------------------------------------------- 1 | 2 | import itunesiap 3 | import datetime 4 | import pytz 5 | import six 6 | 7 | import pytest 8 | 9 | 10 | rfc3339_to_datetime = itunesiap.receipt._rfc3339_to_datetime 11 | ms_to_datetime = itunesiap.receipt._ms_to_datetime 12 | 13 | 14 | def test_to_datetime(): 15 | d1 = rfc3339_to_datetime(u'1970-01-01 00:00:00 Etc/GMT') 16 | d2 = ms_to_datetime(0) 17 | assert d1 == d2 18 | 19 | d1 = rfc3339_to_datetime(u'2017-09-27 15:04:30 Etc/GMT') 20 | d2 = ms_to_datetime(1506524670000) 21 | assert d1 == d2 22 | 23 | 24 | def test_rfc3339_to_datetime(): 25 | """Test to parse string dates to python dates""" 26 | d = rfc3339_to_datetime(u'2013-01-01T00:00:00+09:00') 27 | assert (d.year, d.month, d.day) == (2013, 1, 1) 28 | assert d.tzinfo._offset == datetime.timedelta(0, 9 * 3600) 29 | 30 | d = rfc3339_to_datetime(u'2013-01-01 00:00:00 Etc/GMT') 31 | assert (d.year, d.month, d.day) == (2013, 1, 1) 32 | assert d.tzinfo._utcoffset == datetime.timedelta(0) 33 | 34 | d = rfc3339_to_datetime(u'2013-01-01 00:00:00 America/Los_Angeles') 35 | assert (d.year, d.month, d.day) == (2013, 1, 1) 36 | assert d.tzinfo == pytz.timezone('America/Los_Angeles') 37 | 38 | with pytest.raises(ValueError): 39 | assert rfc3339_to_datetime(u'wrong date') 40 | 41 | 42 | def test_autorenew_general(itunes_autorenew_response): 43 | response = itunesiap.Response(itunes_autorenew_response) 44 | assert response.status == 0 45 | assert response.receipt # definitely, no common sense through versions 46 | 47 | 48 | def test_autorenew_latest(itunes_autorenew_response3): 49 | response = itunesiap.Response(itunes_autorenew_response3) 50 | assert response.status == 0 51 | receipt = response.receipt 52 | assert receipt.quantity == 1 53 | assert isinstance(receipt.purchase_date, datetime.datetime) 54 | assert isinstance(receipt.original_purchase_date, datetime.datetime) 55 | assert receipt.expires_date.date() == datetime.date(2017, 9, 27) 56 | assert receipt.expires_date_ms == 1506524970000 57 | 58 | 59 | def test_autorenew_middleage(itunes_autorenew_response2): 60 | response = itunesiap.Response(itunes_autorenew_response2) 61 | assert isinstance(response.latest_receipt, six.string_types) 62 | 63 | receipt = response.receipt 64 | assert isinstance(receipt, itunesiap.receipt.Receipt) 65 | assert receipt.app_item_id == 0 # 0 only for sandobx 66 | assert receipt.bundle_id == 'com.example.app' 67 | assert receipt.application_version == '8' 68 | assert receipt.version_external_identifier == 0 # 0 only for sandobx 69 | assert receipt.receipt_creation_date.date() == datetime.date(2017, 7, 25) 70 | assert receipt.receipt_creation_date_ms == 1500973280000 71 | itunesiap.receipt.WARN_UNDOCUMENTED_FIELDS = False 72 | assert receipt.request_date.date() == datetime.date(2017, 7, 27) 73 | assert receipt.request_date_ms == 1501149119587 74 | assert receipt.original_purchase_date.date() == datetime.date(2013, 8, 1) 75 | assert receipt.original_purchase_date_ms == 1375340400000 76 | itunesiap.receipt.WARN_UNDOCUMENTED_FIELDS = True 77 | assert receipt.original_application_version == '1.0' 78 | 79 | in_app = receipt.last_in_app 80 | assert in_app.quantity == 1 81 | assert in_app.product_id == 'testproduct' 82 | assert in_app.transaction_id == '1000000318407192' 83 | assert in_app.original_transaction_id == '1000000318012065' 84 | assert in_app.purchase_date.date() == datetime.date(2017, 7, 25) 85 | assert in_app.purchase_date_ms == 1500973279000 86 | assert in_app.original_purchase_date.date() == datetime.date(2017, 7, 24) 87 | assert in_app.original_purchase_date_ms == 1500884005000 88 | assert in_app.expires_date.date() == datetime.date(2017, 7, 25) 89 | assert in_app.expires_date_ms == 1500973579000 90 | assert in_app.web_order_line_item_id == '1000000035713887' 91 | assert in_app.is_trial_period is False 92 | 93 | # latest_receipt_info 94 | purchase = response.latest_receipt_info[-1] 95 | 96 | assert isinstance(purchase, itunesiap.receipt.Purchase) 97 | assert purchase.quantity == 1 98 | assert purchase.product_id == 'testproduct' 99 | assert purchase.transaction_id == '1000000318420598' 100 | assert purchase.original_transaction_id == '1000000318012065' 101 | assert purchase.purchase_date.date() == datetime.date(2017, 7, 25) 102 | assert purchase.purchase_date_ms == 1500974910000 103 | assert purchase.original_purchase_date.date() == datetime.date(2017, 7, 24) 104 | assert purchase.original_purchase_date_ms == 1500884005000 105 | assert purchase.expires_date.date() == datetime.date(2017, 7, 25) 106 | assert purchase.expires_date_ms == 1500975210000 107 | assert purchase.web_order_line_item_id == '1000000035725368' 108 | assert purchase.is_trial_period is False 109 | 110 | pending_info = response.pending_renewal_info[0] 111 | assert isinstance(pending_info, itunesiap.receipt.PendingRenewalInfo) 112 | assert pending_info.expiration_intent == 1 113 | assert pending_info.auto_renew_product_id == 'testproduct' 114 | assert pending_info.is_in_billing_retry_period == 0 115 | assert pending_info.auto_renew_status == 0 116 | 117 | assert receipt.in_app == response.latest_receipt_info[:len(receipt.in_app)] 118 | 119 | 120 | def test_autorenew_legacy(itunes_autorenew_response_legacy): 121 | response = itunesiap.Response(itunes_autorenew_response_legacy) 122 | assert response.receipt.single_purchase == response.latest_receipt_info 123 | purchase = response.receipt.single_purchase 124 | with pytest.raises((OverflowError, ValueError)): 125 | rfc3339_to_datetime(purchase._expires_date) 126 | assert purchase.expires_date.date() == datetime.date(2012, 12, 2) 127 | assert purchase.expires_date == rfc3339_to_datetime(purchase._expires_date_formatted) 128 | 129 | assert isinstance(purchase.original_purchase_date, datetime.datetime) 130 | assert isinstance(purchase.transaction_id, six.string_types) 131 | assert isinstance(purchase.quantity, six.integer_types) 132 | assert isinstance(purchase.purchase_date, datetime.datetime) 133 | assert isinstance(purchase.web_order_line_item_id, six.string_types) 134 | assert isinstance(purchase.unique_identifier, six.string_types) 135 | 136 | 137 | def test_autorenew_receipt1(itunes_autorenew_response1): 138 | response = itunesiap.Response(itunes_autorenew_response1) 139 | 140 | assert response.status == 0 # 0 is normal 141 | 142 | # get the in_app property from the respnse 143 | in_app = response.receipt.in_app 144 | assert len(in_app) == 2 145 | 146 | # test that the InApp object was setup correctly 147 | in_app0 = in_app[0] 148 | assert in_app0.product_id == u'org.itunesiap' 149 | assert in_app0.original_transaction_id == u'1000000155715958' 150 | assert in_app0.quantity == 1 151 | assert isinstance(in_app0.is_trial_period, bool) 152 | assert not in_app0.is_trial_period # is_trial_period is false 153 | assert isinstance(in_app0.original_purchase_date_ms, int) 154 | assert in_app0.original_purchase_date_ms == 1432002585000 155 | assert isinstance(in_app0.purchase_date_ms, int) 156 | assert in_app0.purchase_date_ms == 1432005669000 157 | assert in_app0.purchase_date == datetime.datetime(2013, 5, 19, 3, 21, 9).replace(tzinfo=pytz.UTC) 158 | assert in_app0._purchase_date == '2013-05-19 03:21:09 Etc/GMT' 159 | assert in_app0._purchase_date == in_app0['purchase_date'] 160 | 161 | # and that the last_in_app alias is set up correctly 162 | assert response.receipt.last_in_app.original_transaction_id == '1000000155718067' 163 | 164 | with pytest.raises(AttributeError): 165 | assert response.receipt.in_app[0].expires_date 166 | # ensure we can also catch this with KeyError due to backward compatibility 167 | with pytest.raises(KeyError): 168 | assert response.receipt.in_app[0].expires_date 169 | 170 | 171 | def test_legacy_receipt1(itunes_response_legacy1): 172 | """Test legacy receipt responses""" 173 | response = itunesiap.Response(itunes_response_legacy1) 174 | 175 | assert response.status == 0 # 0 is normal 176 | 177 | in_app = response.receipt.last_in_app 178 | assert in_app.product_id == in_app._product_id == u'BattleGold50' # 179 | assert in_app.original_transaction_id == in_app._original_transaction_id == u'1000000056161764' # original transaction id 180 | assert in_app._quantity == u'1' # check quantity 181 | assert in_app.quantity == 1 182 | assert in_app._purchase_date == u'2012-09-21 01:31:38 Etc/GMT' 183 | 184 | assert in_app._unique_identifier == u'42c1b3d45563820dd9a59c79a75641001fc85e39' 185 | assert in_app._unique_identifier == in_app.unique_identifier 186 | 187 | 188 | def test_legacy_receipt2(itunes_response_legacy2): 189 | response = itunesiap.Response(itunes_response_legacy2) 190 | 191 | assert response.status == 0 # 0 is normal 192 | 193 | in_app = response.receipt.last_in_app 194 | assert in_app._product_id == u'TestProduction1' # 195 | assert in_app._original_transaction_id == u'1000000012345678' # original transaction id 196 | assert in_app._quantity == u'1' # check quantity 197 | assert in_app._unique_identifier == u'bcbdb3d45543920dd9sd5c79a72948001fc22a39' 198 | assert in_app._unique_identifier == in_app.unique_identifier 199 | 200 | 201 | def test_object_mapper(): 202 | response = itunesiap.Response({'status': 21007, 'unknown': 0}) 203 | assert response.status == 21007 # normal access 204 | with pytest.raises(AttributeError): 205 | assert response.unknown 206 | assert response._unknown == 0 # ok but warning 207 | with pytest.raises(AttributeError): 208 | assert response._unknown_field 209 | with pytest.raises(AttributeError): 210 | assert response.latest_receipt # opaque field 211 | with pytest.raises(AttributeError): 212 | assert response.receipt # adapter field 213 | with pytest.raises(AttributeError): 214 | assert response.latest_receipt_info # manually defined field 215 | -------------------------------------------------------------------------------- /itunesiap/receipt.py: -------------------------------------------------------------------------------- 1 | """:mod:`itunesiap.receipt` 2 | 3 | A successful response returns a JSON object including receipts. To manipulate 4 | them in convinient way, `itunes-iap` wrapped it with :class:`ObjectMapper`. 5 | """ 6 | import datetime 7 | import warnings 8 | import pytz 9 | import dateutil.parser 10 | import json 11 | from collections import defaultdict 12 | from prettyexc import PrettyException 13 | 14 | from .tools import lazy_property 15 | 16 | __all__ = ('WARN_UNDOCUMENTED_FIELDS', 'Response', 'Receipt', 'InApp') 17 | 18 | 19 | WARN_UNDOCUMENTED_FIELDS = True 20 | WARN_UNLISTED_FIELDS = True 21 | 22 | _warned_undocumented_fields = defaultdict(bool) 23 | _warned_unlisted_field = defaultdict(bool) 24 | 25 | ''' 26 | class ExpirationIntent(Enum): 27 | CustomerCanceledTheirSubscription = 1 28 | BillingError = 2 29 | CustumerDidNotAgreeToARecentPriceIncrease = 3 30 | ProductWasNotAvailableForPurchaseAtTheTimeOfRenewal = 4 31 | UnknownError = 5 32 | ''' 33 | 34 | 35 | class MissingFieldError(PrettyException, AttributeError, KeyError): 36 | """A Backward compatibility error.""" 37 | 38 | 39 | def _rfc3339_to_datetime(value): 40 | """Try to parse Apple iTunes receipt date format. 41 | 42 | By reference, they insists it is rfc3339: 43 | 44 | https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW4 45 | 46 | Though data I got from apple server does not. So the strategy is: 47 | 48 | - Give them a chance anyway. 49 | - Or split timezone string and read it from pytz. 50 | """ 51 | try: 52 | d = dateutil.parser.parse(value) 53 | except ValueError as e: 54 | value, timezone = value.rsplit(' ', 1) 55 | try: 56 | d = dateutil.parser.parse(value + '+00:00') 57 | except ValueError: 58 | raise e 59 | d = d.replace(tzinfo=pytz.timezone(timezone)) 60 | return d 61 | 62 | 63 | def _ms_to_datetime(value): 64 | nd = datetime.datetime.utcfromtimestamp(int(value) / 1000) 65 | ad = nd.replace(tzinfo=pytz.UTC) 66 | return ad 67 | 68 | 69 | def _to_bool(data): 70 | assert data in ('true', 'false'), \ 71 | ("Cannot convert {0}, " 72 | "acceptable values are true' and 'false'".format(data)) 73 | return json.loads(data) 74 | 75 | 76 | class ObjectMapper(object): 77 | """A pretty interface for decoded receipt object. 78 | 79 | `__DOCUMENTED_FIELDS__` and `__UNDOCUMENTED_FIELDS__` are managed lists of 80 | field names. They are regarded as safe values and guaranteed to be 81 | converted in python representation when needed. 82 | When a field exists in `__OPAQUE_FIELDS__`, its result will be redirected. 83 | The common type is :class:`str`. 84 | When a field exists in `__FIELD_ADAPTERS__`, it will be converted to 85 | corresponding python data representation. 86 | 87 | To access to the converted value, use a dictionary key as an attribute name. 88 | For example, the key `receipt` is accessible by: 89 | 90 | .. sourcecode:: python 91 | 92 | >>> mapper.receipt # return converted python object Receipt 93 | >>> # == Receipt(mapper['receipt']) 94 | 95 | To access to the raw JSON value, use a dictionary key as an attribute name 96 | but with the prefix `_`. For example, the key `receipt` is accessible by: 97 | 98 | >>> mapper._receipt # return converted python object Receipt 99 | >>> # == mapper['receipt'] 100 | 101 | :param dict data: A JSON object. 102 | :return: :class:`ObjectMapper` 103 | """ 104 | __OPAQUE_FIELDS__ = frozenset([]) 105 | __FIELD_ADAPTERS__ = {} 106 | __DOCUMENTED_FIELDS__ = frozenset([]) 107 | __UNDOCUMENTED_FIELDS__ = frozenset([]) 108 | 109 | def __init__(self, data): 110 | self._ = data 111 | 112 | def __repr__(self): 113 | return u'<{self.__class__.__name__}({self._})>'.format(self=self) 114 | 115 | def __getitem__(self, item): 116 | return self._[item] 117 | 118 | def __contains__(self, item): 119 | return item in self._ 120 | 121 | def __getattr__(self, name): 122 | try: 123 | return self.__getattribute__(name) 124 | except AttributeError: 125 | if name.startswith('_'): 126 | key = name[1:] 127 | if key in self.__DOCUMENTED_FIELDS__: 128 | pass 129 | elif key in self.__UNDOCUMENTED_FIELDS__: 130 | self.warn_undocumented(key) 131 | else: 132 | self.warn_unlisted(key) 133 | try: 134 | return self._[key] 135 | except KeyError: 136 | raise MissingFieldError(name) 137 | 138 | if name in self.__DOCUMENTED_FIELDS__: 139 | pass 140 | elif name in self.__UNDOCUMENTED_FIELDS__: 141 | self.warn_undocumented(name) 142 | else: 143 | self.warn_unlisted(name) 144 | 145 | if name in self.__OPAQUE_FIELDS__: 146 | def _get(_self): 147 | try: 148 | value = _self._[name] 149 | except KeyError: 150 | raise MissingFieldError(name) 151 | return value 152 | setattr(self.__class__, name, property(_get)) 153 | elif name in self.__FIELD_ADAPTERS__: 154 | adapter = self.__FIELD_ADAPTERS__[name] 155 | if isinstance(adapter, tuple): 156 | data_key, transform = adapter 157 | else: 158 | data_key = name 159 | transform = adapter 160 | 161 | def _get(_self): 162 | try: 163 | value = _self._[data_key] 164 | except KeyError: 165 | raise MissingFieldError(name) 166 | return transform(value) 167 | setattr(self.__class__, name, lazy_property(_get)) 168 | else: 169 | pass # unhandled. raise AttributeError 170 | return self.__getattribute__(name) 171 | 172 | @classmethod 173 | def from_list(cls, data_list): 174 | return [cls(data) for data in data_list] 175 | 176 | @staticmethod 177 | def warn_undocumented(name): 178 | if not WARN_UNDOCUMENTED_FIELDS or _warned_undocumented_fields[name]: 179 | return 180 | warnings.warn( 181 | "The given field '{name}' is an undocumented field. " 182 | "The behavior is neither documented nor guaranteed by Apple. " 183 | "To suppress further warnings, set the " 184 | "`itunesiap.receipt.WARN_UNDOCUMENTED_FIELDS` False" 185 | .format(name=name)) 186 | _warned_undocumented_fields[name] = True 187 | 188 | @staticmethod 189 | def warn_unlisted(name): 190 | if not WARN_UNLISTED_FIELDS or _warned_unlisted_field[name]: 191 | return 192 | warnings.warn( 193 | "The given field '{name}' is an unlisted field. " 194 | "If the field actually exists, ignore this warning message. " 195 | "To suppress further warnings, please report it to itunes-iap " 196 | "project or set the `itunesiap.receipt.WARN_UNLISTED_FIELDS` " 197 | "False.".format(name=name)) 198 | _warned_unlisted_field[name] = True 199 | 200 | 201 | class Receipt(ObjectMapper): 202 | """The actual receipt. 203 | 204 | The receipt may hold only one purchase directly in receipt object or may 205 | hold multiple purchases in `in_app` key. 206 | This object encapsulate it to list of :class:`InApp` object in `in_app` 207 | property. 208 | 209 | :see: ``_ 210 | """ 211 | __OPAQUE_FIELDS__ = frozenset([ 212 | # app receipt fields 213 | 'bundle_id', 214 | 'application_version', 215 | 'original_application_version', 216 | # in-app purchase receipt fields 217 | 'product_id', 218 | 'transaction_id', 219 | 'original_transaction_id', 220 | 'expires_date_formatted', 221 | 'app_item_id', 222 | 'version_external_identifier', 223 | 'web_order_line_item_id', 224 | 'auto_renew_product_id', 225 | ]) 226 | __FIELD_ADAPTERS__ = { 227 | # app receipt fields 228 | 'receipt_creation_date': _rfc3339_to_datetime, 229 | 'receipt_creation_date_ms': int, 230 | 'expiration_date': _rfc3339_to_datetime, 231 | 'expiration_date_ms': int, 232 | # in-app purchase receipt fields 233 | 'quantity': int, 234 | 'purchase_date': _rfc3339_to_datetime, 235 | 'purchase_date_ms': int, 236 | 'original_purchase_date': _rfc3339_to_datetime, 237 | 'original_purchase_date_ms': int, 238 | 'expires_date': _ms_to_datetime, 239 | 'expires_date_ms': ('expires_date', int), 240 | 'expiration_intent': int, 241 | 'is_in_billing_retry_period': _to_bool, 242 | 'is_in_intro_offer_period': _to_bool, 243 | 'cancellation_date': _rfc3339_to_datetime, 244 | 'cancellation_reason': int, 245 | 'auto_renew_status': int, 246 | 'price_consent_status': int, 247 | 'request_date': _rfc3339_to_datetime, 248 | 'request_date_ms': int, 249 | } 250 | __DOCUMENTED_FIELDS__ = frozenset([ 251 | # app receipt fields 252 | 'bundle_id', 253 | 'in_app', 254 | 'application_version', 255 | 'original_application_version', 256 | 'receipt_creation_date', 257 | 'expiration_date', 258 | # in-app purchase receipt fields 259 | 'quantity', 260 | 'product_id', 261 | 'transaction_id', 262 | 'original_transaction_id', 263 | 'purchase_date', # _formatted value 264 | 'original_purchase_date', 265 | 'expires_date', # _ms value 266 | 'is_in_billing_retry_period', 267 | 'is_in_intro_offer_period', 268 | 'cancellation_date', 269 | 'cancellation_reason', 270 | 'app_item_id', 271 | 'version_external_identifier', 272 | 'web_order_line_item_id', 273 | 'auto_renew_status', 274 | 'auto_renew_product_id', 275 | 'price_consent_status', 276 | ]) 277 | __UNDOCUMENTED_FIELDS__ = frozenset([ 278 | # app receipt fields 279 | 'request_date', 280 | 'request_date_ms', 281 | 'version_external_identifier', 282 | 'receipt_creation_date_ms', 283 | 'expiration_date_ms', 284 | # in-app purchase receipt fields 285 | 'purchase_date_ms', 286 | 'original_purchase_date_ms', 287 | 'expires_date_formatted', 288 | 'unique_identifier', 289 | ]) 290 | 291 | @lazy_property 292 | def single_purchase(self): 293 | return Purchase(self._) 294 | 295 | @lazy_property 296 | def in_app(self): 297 | """The list of purchases. If the receipt has receipt keys in the 298 | receipt body, it still will be wrapped as an :class:`InApp` and consists 299 | of this property 300 | """ 301 | if 'in_app' in self._: 302 | return list(map(InApp, self._in_app)) 303 | else: 304 | return [self.single_purchase] 305 | 306 | @property 307 | def last_in_app(self): 308 | """The last item in `in_app` property order by purchase_date.""" 309 | return sorted( 310 | self.in_app, key=lambda x: x['original_purchase_date_ms'])[-1] 311 | 312 | 313 | class Purchase(ObjectMapper): 314 | """The individual purchases. 315 | 316 | The major keys are `quantity`, `product_id` and `transaction_id`. 317 | `quantty` and `product_id` mean what kind of product and 318 | and how many of them the customer bought. `unique_identifier` and 319 | `transaction_id` is used to check where it is processed and track related 320 | purchases. 321 | 322 | For the detail, see also Apple docs. 323 | 324 | Any `date` related keys will be converted to python 325 | :class:`datetime.datetime` object. The quantity and any `date_ms` related 326 | keys will be converted to python :class:`int`. 327 | """ 328 | __OPAQUE_FIELDS__ = frozenset([ 329 | 'product_id', 330 | 'transaction_id', 331 | 'original_transaction_id', 332 | 'web_order_line_item_id', 333 | 'unique_identifier', 334 | 'expires_date_formatted', 335 | ]) 336 | __FIELD_ADAPTERS__ = { 337 | 'quantity': int, 338 | 'purchase_date': _rfc3339_to_datetime, 339 | 'purchase_date_ms': int, 340 | 'original_purchase_date': _rfc3339_to_datetime, 341 | 'original_purchase_date_ms': int, 342 | 'expires_date': _rfc3339_to_datetime, 343 | 'expires_date_ms': int, 344 | 'is_trial_period': _to_bool, 345 | 'cancellation_date': _rfc3339_to_datetime, 346 | 'cancellation_date_ms': int, 347 | 'cancellation_reason': int, 348 | } 349 | __DOCUMENTED_FIELDS__ = frozenset([ 350 | 'quantity', 351 | 'product_id', 352 | 'transaction_id', 353 | 'original_transaction_id', 354 | 'purchase_date', 355 | 'original_purchase_date', 356 | 'expires_date', 357 | 'is_trial_period', 358 | 'cancellation_date', 359 | 'cancellation_reason', 360 | 'web_order_line_item_id', 361 | ]) 362 | __UNDOCUMENTED_FIELDS__ = frozenset([ 363 | 'unique_identifier', 364 | 'purchase_date_ms', 365 | 'original_purchase_date_ms', 366 | 'cancellation_date_ms', 367 | 'expires_date_formatted', # legacy receipts has this field as actual "expires_date" 368 | ]) 369 | 370 | def __eq__(self, other): 371 | if not isinstance(other, Purchase): # pragma: no cover 372 | return False 373 | return self._ == other._ 374 | 375 | @lazy_property 376 | def expires_date(self): 377 | if 'expires_date_formatted' in self: 378 | return _rfc3339_to_datetime(self['expires_date_formatted']) 379 | try: 380 | value = self['expires_date'] 381 | except KeyError: 382 | raise MissingFieldError('expires_date') 383 | try: 384 | int(value) 385 | except ValueError: 386 | return _rfc3339_to_datetime(value) 387 | else: 388 | return _ms_to_datetime(value) 389 | 390 | 391 | class InApp(Purchase): 392 | pass 393 | 394 | 395 | class PendingRenewalInfo(ObjectMapper): 396 | __OPAQUE_FIELDS__ = frozenset([ 397 | 'auto_renew_product_id', 398 | ]) 399 | __FIELD_ADAPTERS__ = { 400 | 'auto_renew_status': int, 401 | 'expiration_intent': int, 402 | 'is_in_billing_retry_period': int, 403 | } 404 | __DOCUMENTED_FIELDS__ = frozenset([ 405 | 'expiration_intent', 406 | 'auto_renew_status', 407 | 'auto_renew_product_id', 408 | 'is_in_billing_retry_period', 409 | ]) 410 | __UNDOCUMENTED_FIELDS__ = frozenset([ 411 | ]) 412 | 413 | 414 | class Response(ObjectMapper): 415 | """The root response. 416 | 417 | About the value of status: 418 | - See https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1 419 | """ 420 | __OPAQUE_FIELDS__ = frozenset([ 421 | 'latest_receipt', 422 | ]) 423 | __FIELD_ADAPTERS__ = { 424 | 'status': int, 425 | 'receipt': Receipt, 426 | 'pending_renewal_info': PendingRenewalInfo.from_list, 427 | } 428 | __DOCUMENTED_FIELDS__ = frozenset([ 429 | 'status', 430 | 'receipt', 431 | 'latest_receipt', 432 | 'latest_receipt_info', 433 | # 'latest_expired_receipt_info', 434 | 'pending_renewal_info', 435 | # is-retryable 436 | ]) 437 | __UNDOCUMENTED_FIELDS__ = frozenset([ 438 | ]) 439 | 440 | @lazy_property 441 | def latest_receipt_info(self): 442 | if 'latest_receipt_info' not in self: 443 | # not an auto-renew purchase 444 | raise MissingFieldError('latest_receipt_info') 445 | info = self['latest_receipt_info'] 446 | if isinstance(info, dict): # iOS6 style 447 | return Purchase(info) 448 | elif isinstance(info, list): # iOS7 style 449 | return InApp.from_list(info) 450 | else: # pragma: no cover 451 | assert False 452 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import itunesiap 4 | import pytest 5 | from pytest_lazyfixture import lazy_fixture 6 | 7 | 8 | def _raw_receipt_legacy(): 9 | return '''ewoJInNpZ25hdHVyZSIgPSAiQW1vSjJDNFhra1hXcngwbDBwMUVCMkhqdndWRkJPN3NxaHRPYVpYWXNtd29PblU4dkNYNWZJWFV6SmpwWVpwVGJ1bTJhWW5kci9uOHlBc2czUXc0WUZHMUtCbEpLSjU2c1gzcEpmWTRZd2hEMmJsdm1lZVowZ0FXKzNiajBRWGVjUWJORTk5b2duK09janY2U3dFSEdpdkRIY0FRNzBiMTYxekdpbTk2WHVKTkFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBNUxUSXdJREU0T2pNeE9qTTRJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeUlpQTlJQ0kwTW1NeFlqTmtORFUxTmpNNE1qQmtaRGxoTlRsak56bGhOelUyTkRFd01ERm1ZemcxWlRNNUlqc0tDU0p2Y21sbmFXNWhiQzEwY21GdWMyRmpkR2x2YmkxcFpDSWdQU0FpTVRBd01EQXdNREExTmpFMk1UYzJOQ0k3Q2draVluWnljeUlnUFNBaU1TNHdJanNLQ1NKMGNtRnVjMkZqZEdsdmJpMXBaQ0lnUFNBaU1UQXdNREF3TURBMU5qRTJNVGMyTkNJN0Nna2ljWFZoYm5ScGRIa2lJRDBnSWpFaU93b0pJbTl5YVdkcGJtRnNMWEIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFek5EZ3hPVEV3T1RneE9USWlPd29KSW5CeWIyUjFZM1F0YVdRaUlEMGdJa0poZEhSc1pVZHZiR1ExTUNJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTlRVME5EazVNekExSWpzS0NTSmlhV1FpSUQwZ0ltTnZiUzUyWVc1cGJHeGhZbkpsWlhwbExtbG5kVzVpWVhSMGJHVWlPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVXRiWE1pSUQwZ0lqRXpORGd4T1RFd09UZ3hPVElpT3dvSkluQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVEl0TURrdE1qRWdNREU2TXpFNk16Z2dSWFJqTDBkTlZDSTdDZ2tpY0hWeVkyaGhjMlV0WkdGMFpTMXdjM1FpSUQwZ0lqSXdNVEl0TURrdE1qQWdNVGc2TXpFNk16Z2dRVzFsY21sallTOU1iM05mUVc1blpXeGxjeUk3Q2draWIzSnBaMmx1WVd3dGNIVnlZMmhoYzJVdFpHRjBaU0lnUFNBaU1qQXhNaTB3T1MweU1TQXdNVG96TVRvek9DQkZkR012UjAxVUlqc0tmUT09IjsKCSJlbnZpcm9ubWVudCIgPSAiU2FuZGJveCI7CgkicG9kIiA9ICIxMDAiOwoJInNpZ25pbmctc3RhdHVzIiA9ICIwIjsKfQ==''' # noqa 10 | 11 | 12 | raw_receipt_legacy = pytest.fixture(scope='session')(_raw_receipt_legacy) 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def itunes_response_legacy1(raw_receipt_legacy): 17 | response = itunesiap.verify(raw_receipt_legacy, env=itunesiap.env.sandbox) 18 | return getattr(response, '_') 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def itunes_response_legacy2(): 23 | return { 24 | u'status': 0, 25 | u'receipt': { 26 | u'purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 27 | u'product_id': u'TestProduction1', 28 | u'original_transaction_id': u'1000000012345678', 29 | u'unique_identifier': u'bcbdb3d45543920dd9sd5c79a72948001fc22a39', 30 | u'original_purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 31 | u'original_purchase_date': u'2013-01-01 00:00:00 Etc/GMT', 32 | u'bvrs': u'1.0', 33 | u'original_purchase_date_ms': u'1348200000000', 34 | u'purchase_date': u'2013-01-01 00:00:00 Etc/GMT', 35 | u'item_id': u'500000000', 36 | u'purchase_date_ms': u'134820000000', 37 | u'bid': u'org.youknowone.itunesiap', 38 | u'transaction_id': u'1000000012345678', 39 | u'quantity': u'1' 40 | } 41 | } 42 | 43 | 44 | @pytest.fixture(scope='session') 45 | def itunes_autorenew_response_legacy(): 46 | """https://gist.github.com/lxcid/4187607""" 47 | return json.loads('''{ 48 | "status": 0, 49 | "latest_receipt_info": { 50 | "original_purchase_date_ms": "1354432554000", 51 | "original_purchase_date_pst": "2012-12-01 23:15:54 America/Los_Angeles", 52 | "transaction_id": "1000000059630000", 53 | "quantity": "1", 54 | "bid": "com.example.001", 55 | "original_transaction_id": "1000000059630000", 56 | "bvrs": "7", 57 | "expires_date_formatted": "2012-12-02 08:15:54 Etc/GMT", 58 | "purchase_date": "2012-12-02 07:15:54 Etc/GMT", 59 | "expires_date": "1354436154000", 60 | "product_id": "com.example.premium.1y", 61 | "purchase_date_ms": "1354432554000", 62 | "expires_date_formatted_pst": "2012-12-02 00:15:54 America/Los_Angeles", 63 | "purchase_date_pst": "2012-12-01 23:15:54 America/Los_Angeles", 64 | "original_purchase_date": "2012-12-02 07:15:54 Etc/GMT", 65 | "item_id": "580190000", 66 | "web_order_line_item_id": "1000000026430000", 67 | "unique_identifier": "0000b0090000" 68 | }, 69 | "receipt": { 70 | "original_purchase_date_ms": "1354432554000", 71 | "original_purchase_date_pst": "2012-12-01 23:15:54 America/Los_Angeles", 72 | "transaction_id": "1000000059630000", 73 | "quantity": "1", 74 | "bid": "com.example.001", 75 | "original_transaction_id": "1000000059630000", 76 | "bvrs": "7", 77 | "expires_date_formatted": "2012-12-02 08:15:54 Etc/GMT", 78 | "purchase_date": "2012-12-02 07:15:54 Etc/GMT", 79 | "expires_date": "1354436154000", 80 | "product_id": "com.example.premium.1y", 81 | "purchase_date_ms": "1354432554000", 82 | "expires_date_formatted_pst": "2012-12-02 00:15:54 America/Los_Angeles", 83 | "purchase_date_pst": "2012-12-01 23:15:54 America/Los_Angeles", 84 | "original_purchase_date": "2012-12-02 07:15:54 Etc/GMT", 85 | "item_id": "580190000", 86 | "web_order_line_item_id": "1000000026430000", 87 | "unique_identifier": "0000b0090000" 88 | }, 89 | "latest_receipt": "__ACTUAL_BASE64_ENCODED_RECEIPT" 90 | }''') 91 | 92 | 93 | @pytest.fixture(scope='session') 94 | def itunes_autorenew_response1(): 95 | return { 96 | u'status': 0, 97 | u'receipt': { 98 | u'original_purchase_date_pst': u'2013-01-01 00:00:00 America/Los_Angeles', 99 | u'version_external_identifier': 0, 100 | u'original_purchase_date': u'2013-01-01 07:00:00 Etc/GMT', 101 | u'in_app': [ 102 | { 103 | u'is_trial_period': u'false', 104 | u'purchase_date_pst': u'2013-05-18 20:21:09 America/Los_Angeles', 105 | u'product_id': u'org.itunesiap', 106 | u'original_transaction_id': u'1000000155715958', 107 | u'original_purchase_date_pst': u'2013-05-18 19:29:45 America/Los_Angeles', 108 | u'original_purchase_date': u'2013-05-19 02:29:45 Etc/GMT', 109 | u'original_purchase_date_ms': u'1432002585000', 110 | u'purchase_date': u'2013-05-19 03:21:09 Etc/GMT', 111 | u'purchase_date_ms': u'1432005669000', 112 | u'transaction_id': u'1000000155715958', 113 | u'quantity': u'1' 114 | }, 115 | { 116 | u'is_trial_period': u'false', 117 | u'purchase_date_pst': u'2013-05-19 20:21:09 America/Los_Angeles', 118 | u'product_id': u'org.itunesiap', 119 | u'original_transaction_id': u'1000000155718067', 120 | u'original_purchase_date_pst': u'2013-05-18 19:37:10 America/Los_Angeles', 121 | u'original_purchase_date': u'2013-05-19 02:37:10 Etc/GMT', 122 | u'original_purchase_date_ms': u'1432003030000', 123 | u'purchase_date': u'2013-05-19 03:21:09 Etc/GMT', 124 | u'purchase_date_ms': u'1432005669000', 125 | u'transaction_id': u'1000000155718067', 126 | u'quantity': u'1' 127 | } 128 | ] 129 | } 130 | } 131 | 132 | 133 | @pytest.fixture(scope='session') 134 | def itunes_autorenew_response2(): 135 | """Contributed by Jonas Petersen @jox""" 136 | return json.loads(r'''{ 137 | "status": 0, 138 | "environment": "Sandbox", 139 | "receipt": { 140 | "receipt_type": "ProductionSandbox", 141 | "adam_id": 0, 142 | "app_item_id": 0, 143 | "bundle_id": "com.example.app", 144 | "application_version": "8", 145 | "download_id": 0, 146 | "version_external_identifier": 0, 147 | "receipt_creation_date": "2017-07-25 09:01:20 Etc/GMT", 148 | "receipt_creation_date_ms": "1500973280000", 149 | "receipt_creation_date_pst": "2017-07-25 02:01:20 America/Los_Angeles", 150 | "request_date": "2017-07-27 09:51:59 Etc/GMT", 151 | "request_date_ms": "1501149119587", 152 | "request_date_pst": "2017-07-27 02:51:59 America/Los_Angeles", 153 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 154 | "original_purchase_date_ms": "1375340400000", 155 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 156 | "original_application_version": "1.0", 157 | "in_app": [ 158 | { 159 | "quantity": "1", 160 | "product_id": "testproduct", 161 | "transaction_id": "1000000318012065", 162 | "original_transaction_id": "1000000318012065", 163 | "purchase_date": "2017-07-24 08:13:24 Etc/GMT", 164 | "purchase_date_ms": "1500884004000", 165 | "purchase_date_pst": "2017-07-24 01:13:24 America/Los_Angeles", 166 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 167 | "original_purchase_date_ms": "1500884005000", 168 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 169 | "expires_date": "2017-07-24 08:18:24 Etc/GMT", 170 | "expires_date_ms": "1500884304000", 171 | "expires_date_pst": "2017-07-24 01:18:24 America/Los_Angeles", 172 | "web_order_line_item_id": "1000000035712036", 173 | "is_trial_period": "false" 174 | }, 175 | { 176 | "quantity": "1", 177 | "product_id": "testproduct", 178 | "transaction_id": "1000000318014271", 179 | "original_transaction_id": "1000000318012065", 180 | "purchase_date": "2017-07-24 08:20:19 Etc/GMT", 181 | "purchase_date_ms": "1500884419000", 182 | "purchase_date_pst": "2017-07-24 01:20:19 America/Los_Angeles", 183 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 184 | "original_purchase_date_ms": "1500884005000", 185 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 186 | "expires_date": "2017-07-24 08:25:19 Etc/GMT", 187 | "expires_date_ms": "1500884719000", 188 | "expires_date_pst": "2017-07-24 01:25:19 America/Los_Angeles", 189 | "web_order_line_item_id": "1000000035712037", 190 | "is_trial_period": "false" 191 | }, 192 | { 193 | "quantity": "1", 194 | "product_id": "testproduct", 195 | "transaction_id": "1000000318015678", 196 | "original_transaction_id": "1000000318012065", 197 | "purchase_date": "2017-07-24 08:25:19 Etc/GMT", 198 | "purchase_date_ms": "1500884719000", 199 | "purchase_date_pst": "2017-07-24 01:25:19 America/Los_Angeles", 200 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 201 | "original_purchase_date_ms": "1500884005000", 202 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 203 | "expires_date": "2017-07-24 08:30:19 Etc/GMT", 204 | "expires_date_ms": "1500885019000", 205 | "expires_date_pst": "2017-07-24 01:30:19 America/Los_Angeles", 206 | "web_order_line_item_id": "1000000035712099", 207 | "is_trial_period": "false" 208 | }, 209 | { 210 | "quantity": "1", 211 | "product_id": "testproduct", 212 | "transaction_id": "1000000318021093", 213 | "original_transaction_id": "1000000318012065", 214 | "purchase_date": "2017-07-24 08:32:23 Etc/GMT", 215 | "purchase_date_ms": "1500885143000", 216 | "purchase_date_pst": "2017-07-24 01:32:23 America/Los_Angeles", 217 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 218 | "original_purchase_date_ms": "1500884005000", 219 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 220 | "expires_date": "2017-07-24 08:37:23 Etc/GMT", 221 | "expires_date_ms": "1500885443000", 222 | "expires_date_pst": "2017-07-24 01:37:23 America/Los_Angeles", 223 | "web_order_line_item_id": "1000000035712148", 224 | "is_trial_period": "false" 225 | }, 226 | { 227 | "quantity": "1", 228 | "product_id": "testproduct", 229 | "transaction_id": "1000000318022372", 230 | "original_transaction_id": "1000000318012065", 231 | "purchase_date": "2017-07-24 08:37:23 Etc/GMT", 232 | "purchase_date_ms": "1500885443000", 233 | "purchase_date_pst": "2017-07-24 01:37:23 America/Los_Angeles", 234 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 235 | "original_purchase_date_ms": "1500884005000", 236 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 237 | "expires_date": "2017-07-24 08:42:23 Etc/GMT", 238 | "expires_date_ms": "1500885743000", 239 | "expires_date_pst": "2017-07-24 01:42:23 America/Los_Angeles", 240 | "web_order_line_item_id": "1000000035712240", 241 | "is_trial_period": "false" 242 | }, 243 | { 244 | "quantity": "1", 245 | "product_id": "testproduct", 246 | "transaction_id": "1000000318024256", 247 | "original_transaction_id": "1000000318012065", 248 | "purchase_date": "2017-07-24 08:42:23 Etc/GMT", 249 | "purchase_date_ms": "1500885743000", 250 | "purchase_date_pst": "2017-07-24 01:42:23 America/Los_Angeles", 251 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 252 | "original_purchase_date_ms": "1500884005000", 253 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 254 | "expires_date": "2017-07-24 08:47:23 Etc/GMT", 255 | "expires_date_ms": "1500886043000", 256 | "expires_date_pst": "2017-07-24 01:47:23 America/Los_Angeles", 257 | "web_order_line_item_id": "1000000035712292", 258 | "is_trial_period": "false" 259 | }, 260 | { 261 | "quantity": "1", 262 | "product_id": "testproduct", 263 | "transaction_id": "1000000318060909", 264 | "original_transaction_id": "1000000318012065", 265 | "purchase_date": "2017-07-24 10:21:48 Etc/GMT", 266 | "purchase_date_ms": "1500891708000", 267 | "purchase_date_pst": "2017-07-24 03:21:48 America/Los_Angeles", 268 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 269 | "original_purchase_date_ms": "1500884005000", 270 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 271 | "expires_date": "2017-07-24 10:26:48 Etc/GMT", 272 | "expires_date_ms": "1500892008000", 273 | "expires_date_pst": "2017-07-24 03:26:48 America/Los_Angeles", 274 | "web_order_line_item_id": "1000000035712361", 275 | "is_trial_period": "false" 276 | }, 277 | { 278 | "quantity": "1", 279 | "product_id": "testproduct", 280 | "transaction_id": "1000000318063451", 281 | "original_transaction_id": "1000000318012065", 282 | "purchase_date": "2017-07-24 10:26:51 Etc/GMT", 283 | "purchase_date_ms": "1500892011000", 284 | "purchase_date_pst": "2017-07-24 03:26:51 America/Los_Angeles", 285 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 286 | "original_purchase_date_ms": "1500884005000", 287 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 288 | "expires_date": "2017-07-24 10:31:51 Etc/GMT", 289 | "expires_date_ms": "1500892311000", 290 | "expires_date_pst": "2017-07-24 03:31:51 America/Los_Angeles", 291 | "web_order_line_item_id": "1000000035713600", 292 | "is_trial_period": "false" 293 | }, 294 | { 295 | "quantity": "1", 296 | "product_id": "testproduct", 297 | "transaction_id": "1000000318065205", 298 | "original_transaction_id": "1000000318012065", 299 | "purchase_date": "2017-07-24 10:31:51 Etc/GMT", 300 | "purchase_date_ms": "1500892311000", 301 | "purchase_date_pst": "2017-07-24 03:31:51 America/Los_Angeles", 302 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 303 | "original_purchase_date_ms": "1500884005000", 304 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 305 | "expires_date": "2017-07-24 10:36:51 Etc/GMT", 306 | "expires_date_ms": "1500892611000", 307 | "expires_date_pst": "2017-07-24 03:36:51 America/Los_Angeles", 308 | "web_order_line_item_id": "1000000035713658", 309 | "is_trial_period": "false" 310 | }, 311 | { 312 | "quantity": "1", 313 | "product_id": "testproduct", 314 | "transaction_id": "1000000318066018", 315 | "original_transaction_id": "1000000318012065", 316 | "purchase_date": "2017-07-24 10:36:51 Etc/GMT", 317 | "purchase_date_ms": "1500892611000", 318 | "purchase_date_pst": "2017-07-24 03:36:51 America/Los_Angeles", 319 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 320 | "original_purchase_date_ms": "1500884005000", 321 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 322 | "expires_date": "2017-07-24 10:41:51 Etc/GMT", 323 | "expires_date_ms": "1500892911000", 324 | "expires_date_pst": "2017-07-24 03:41:51 America/Los_Angeles", 325 | "web_order_line_item_id": "1000000035713715", 326 | "is_trial_period": "false" 327 | }, 328 | { 329 | "quantity": "1", 330 | "product_id": "testproduct", 331 | "transaction_id": "1000000318067267", 332 | "original_transaction_id": "1000000318012065", 333 | "purchase_date": "2017-07-24 10:42:17 Etc/GMT", 334 | "purchase_date_ms": "1500892937000", 335 | "purchase_date_pst": "2017-07-24 03:42:17 America/Los_Angeles", 336 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 337 | "original_purchase_date_ms": "1500884005000", 338 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 339 | "expires_date": "2017-07-24 10:47:17 Etc/GMT", 340 | "expires_date_ms": "1500893237000", 341 | "expires_date_pst": "2017-07-24 03:47:17 America/Los_Angeles", 342 | "web_order_line_item_id": "1000000035713778", 343 | "is_trial_period": "false" 344 | }, 345 | { 346 | "quantity": "1", 347 | "product_id": "testproduct", 348 | "transaction_id": "1000000318069609", 349 | "original_transaction_id": "1000000318012065", 350 | "purchase_date": "2017-07-24 10:47:17 Etc/GMT", 351 | "purchase_date_ms": "1500893237000", 352 | "purchase_date_pst": "2017-07-24 03:47:17 America/Los_Angeles", 353 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 354 | "original_purchase_date_ms": "1500884005000", 355 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 356 | "expires_date": "2017-07-24 10:52:17 Etc/GMT", 357 | "expires_date_ms": "1500893537000", 358 | "expires_date_pst": "2017-07-24 03:52:17 America/Los_Angeles", 359 | "web_order_line_item_id": "1000000035713845", 360 | "is_trial_period": "false" 361 | }, 362 | { 363 | "quantity": "1", 364 | "product_id": "testproduct", 365 | "transaction_id": "1000000318407192", 366 | "original_transaction_id": "1000000318012065", 367 | "purchase_date": "2017-07-25 09:01:19 Etc/GMT", 368 | "purchase_date_ms": "1500973279000", 369 | "purchase_date_pst": "2017-07-25 02:01:19 America/Los_Angeles", 370 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 371 | "original_purchase_date_ms": "1500884005000", 372 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 373 | "expires_date": "2017-07-25 09:06:19 Etc/GMT", 374 | "expires_date_ms": "1500973579000", 375 | "expires_date_pst": "2017-07-25 02:06:19 America/Los_Angeles", 376 | "web_order_line_item_id": "1000000035713887", 377 | "is_trial_period": "false" 378 | } 379 | ] 380 | }, 381 | "latest_receipt_info": [ 382 | { 383 | "quantity": "1", 384 | "product_id": "testproduct", 385 | "transaction_id": "1000000318012065", 386 | "original_transaction_id": "1000000318012065", 387 | "purchase_date": "2017-07-24 08:13:24 Etc/GMT", 388 | "purchase_date_ms": "1500884004000", 389 | "purchase_date_pst": "2017-07-24 01:13:24 America/Los_Angeles", 390 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 391 | "original_purchase_date_ms": "1500884005000", 392 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 393 | "expires_date": "2017-07-24 08:18:24 Etc/GMT", 394 | "expires_date_ms": "1500884304000", 395 | "expires_date_pst": "2017-07-24 01:18:24 America/Los_Angeles", 396 | "web_order_line_item_id": "1000000035712036", 397 | "is_trial_period": "false" 398 | }, 399 | { 400 | "quantity": "1", 401 | "product_id": "testproduct", 402 | "transaction_id": "1000000318014271", 403 | "original_transaction_id": "1000000318012065", 404 | "purchase_date": "2017-07-24 08:20:19 Etc/GMT", 405 | "purchase_date_ms": "1500884419000", 406 | "purchase_date_pst": "2017-07-24 01:20:19 America/Los_Angeles", 407 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 408 | "original_purchase_date_ms": "1500884005000", 409 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 410 | "expires_date": "2017-07-24 08:25:19 Etc/GMT", 411 | "expires_date_ms": "1500884719000", 412 | "expires_date_pst": "2017-07-24 01:25:19 America/Los_Angeles", 413 | "web_order_line_item_id": "1000000035712037", 414 | "is_trial_period": "false" 415 | }, 416 | { 417 | "quantity": "1", 418 | "product_id": "testproduct", 419 | "transaction_id": "1000000318015678", 420 | "original_transaction_id": "1000000318012065", 421 | "purchase_date": "2017-07-24 08:25:19 Etc/GMT", 422 | "purchase_date_ms": "1500884719000", 423 | "purchase_date_pst": "2017-07-24 01:25:19 America/Los_Angeles", 424 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 425 | "original_purchase_date_ms": "1500884005000", 426 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 427 | "expires_date": "2017-07-24 08:30:19 Etc/GMT", 428 | "expires_date_ms": "1500885019000", 429 | "expires_date_pst": "2017-07-24 01:30:19 America/Los_Angeles", 430 | "web_order_line_item_id": "1000000035712099", 431 | "is_trial_period": "false" 432 | }, 433 | { 434 | "quantity": "1", 435 | "product_id": "testproduct", 436 | "transaction_id": "1000000318021093", 437 | "original_transaction_id": "1000000318012065", 438 | "purchase_date": "2017-07-24 08:32:23 Etc/GMT", 439 | "purchase_date_ms": "1500885143000", 440 | "purchase_date_pst": "2017-07-24 01:32:23 America/Los_Angeles", 441 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 442 | "original_purchase_date_ms": "1500884005000", 443 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 444 | "expires_date": "2017-07-24 08:37:23 Etc/GMT", 445 | "expires_date_ms": "1500885443000", 446 | "expires_date_pst": "2017-07-24 01:37:23 America/Los_Angeles", 447 | "web_order_line_item_id": "1000000035712148", 448 | "is_trial_period": "false" 449 | }, 450 | { 451 | "quantity": "1", 452 | "product_id": "testproduct", 453 | "transaction_id": "1000000318022372", 454 | "original_transaction_id": "1000000318012065", 455 | "purchase_date": "2017-07-24 08:37:23 Etc/GMT", 456 | "purchase_date_ms": "1500885443000", 457 | "purchase_date_pst": "2017-07-24 01:37:23 America/Los_Angeles", 458 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 459 | "original_purchase_date_ms": "1500884005000", 460 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 461 | "expires_date": "2017-07-24 08:42:23 Etc/GMT", 462 | "expires_date_ms": "1500885743000", 463 | "expires_date_pst": "2017-07-24 01:42:23 America/Los_Angeles", 464 | "web_order_line_item_id": "1000000035712240", 465 | "is_trial_period": "false" 466 | }, 467 | { 468 | "quantity": "1", 469 | "product_id": "testproduct", 470 | "transaction_id": "1000000318024256", 471 | "original_transaction_id": "1000000318012065", 472 | "purchase_date": "2017-07-24 08:42:23 Etc/GMT", 473 | "purchase_date_ms": "1500885743000", 474 | "purchase_date_pst": "2017-07-24 01:42:23 America/Los_Angeles", 475 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 476 | "original_purchase_date_ms": "1500884005000", 477 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 478 | "expires_date": "2017-07-24 08:47:23 Etc/GMT", 479 | "expires_date_ms": "1500886043000", 480 | "expires_date_pst": "2017-07-24 01:47:23 America/Los_Angeles", 481 | "web_order_line_item_id": "1000000035712292", 482 | "is_trial_period": "false" 483 | }, 484 | { 485 | "quantity": "1", 486 | "product_id": "testproduct", 487 | "transaction_id": "1000000318060909", 488 | "original_transaction_id": "1000000318012065", 489 | "purchase_date": "2017-07-24 10:21:48 Etc/GMT", 490 | "purchase_date_ms": "1500891708000", 491 | "purchase_date_pst": "2017-07-24 03:21:48 America/Los_Angeles", 492 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 493 | "original_purchase_date_ms": "1500884005000", 494 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 495 | "expires_date": "2017-07-24 10:26:48 Etc/GMT", 496 | "expires_date_ms": "1500892008000", 497 | "expires_date_pst": "2017-07-24 03:26:48 America/Los_Angeles", 498 | "web_order_line_item_id": "1000000035712361", 499 | "is_trial_period": "false" 500 | }, 501 | { 502 | "quantity": "1", 503 | "product_id": "testproduct", 504 | "transaction_id": "1000000318063451", 505 | "original_transaction_id": "1000000318012065", 506 | "purchase_date": "2017-07-24 10:26:51 Etc/GMT", 507 | "purchase_date_ms": "1500892011000", 508 | "purchase_date_pst": "2017-07-24 03:26:51 America/Los_Angeles", 509 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 510 | "original_purchase_date_ms": "1500884005000", 511 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 512 | "expires_date": "2017-07-24 10:31:51 Etc/GMT", 513 | "expires_date_ms": "1500892311000", 514 | "expires_date_pst": "2017-07-24 03:31:51 America/Los_Angeles", 515 | "web_order_line_item_id": "1000000035713600", 516 | "is_trial_period": "false" 517 | }, 518 | { 519 | "quantity": "1", 520 | "product_id": "testproduct", 521 | "transaction_id": "1000000318065205", 522 | "original_transaction_id": "1000000318012065", 523 | "purchase_date": "2017-07-24 10:31:51 Etc/GMT", 524 | "purchase_date_ms": "1500892311000", 525 | "purchase_date_pst": "2017-07-24 03:31:51 America/Los_Angeles", 526 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 527 | "original_purchase_date_ms": "1500884005000", 528 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 529 | "expires_date": "2017-07-24 10:36:51 Etc/GMT", 530 | "expires_date_ms": "1500892611000", 531 | "expires_date_pst": "2017-07-24 03:36:51 America/Los_Angeles", 532 | "web_order_line_item_id": "1000000035713658", 533 | "is_trial_period": "false" 534 | }, 535 | { 536 | "quantity": "1", 537 | "product_id": "testproduct", 538 | "transaction_id": "1000000318066018", 539 | "original_transaction_id": "1000000318012065", 540 | "purchase_date": "2017-07-24 10:36:51 Etc/GMT", 541 | "purchase_date_ms": "1500892611000", 542 | "purchase_date_pst": "2017-07-24 03:36:51 America/Los_Angeles", 543 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 544 | "original_purchase_date_ms": "1500884005000", 545 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 546 | "expires_date": "2017-07-24 10:41:51 Etc/GMT", 547 | "expires_date_ms": "1500892911000", 548 | "expires_date_pst": "2017-07-24 03:41:51 America/Los_Angeles", 549 | "web_order_line_item_id": "1000000035713715", 550 | "is_trial_period": "false" 551 | }, 552 | { 553 | "quantity": "1", 554 | "product_id": "testproduct", 555 | "transaction_id": "1000000318067267", 556 | "original_transaction_id": "1000000318012065", 557 | "purchase_date": "2017-07-24 10:42:17 Etc/GMT", 558 | "purchase_date_ms": "1500892937000", 559 | "purchase_date_pst": "2017-07-24 03:42:17 America/Los_Angeles", 560 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 561 | "original_purchase_date_ms": "1500884005000", 562 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 563 | "expires_date": "2017-07-24 10:47:17 Etc/GMT", 564 | "expires_date_ms": "1500893237000", 565 | "expires_date_pst": "2017-07-24 03:47:17 America/Los_Angeles", 566 | "web_order_line_item_id": "1000000035713778", 567 | "is_trial_period": "false" 568 | }, 569 | { 570 | "quantity": "1", 571 | "product_id": "testproduct", 572 | "transaction_id": "1000000318069609", 573 | "original_transaction_id": "1000000318012065", 574 | "purchase_date": "2017-07-24 10:47:17 Etc/GMT", 575 | "purchase_date_ms": "1500893237000", 576 | "purchase_date_pst": "2017-07-24 03:47:17 America/Los_Angeles", 577 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 578 | "original_purchase_date_ms": "1500884005000", 579 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 580 | "expires_date": "2017-07-24 10:52:17 Etc/GMT", 581 | "expires_date_ms": "1500893537000", 582 | "expires_date_pst": "2017-07-24 03:52:17 America/Los_Angeles", 583 | "web_order_line_item_id": "1000000035713845", 584 | "is_trial_period": "false" 585 | }, 586 | { 587 | "quantity": "1", 588 | "product_id": "testproduct", 589 | "transaction_id": "1000000318407192", 590 | "original_transaction_id": "1000000318012065", 591 | "purchase_date": "2017-07-25 09:01:19 Etc/GMT", 592 | "purchase_date_ms": "1500973279000", 593 | "purchase_date_pst": "2017-07-25 02:01:19 America/Los_Angeles", 594 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 595 | "original_purchase_date_ms": "1500884005000", 596 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 597 | "expires_date": "2017-07-25 09:06:19 Etc/GMT", 598 | "expires_date_ms": "1500973579000", 599 | "expires_date_pst": "2017-07-25 02:06:19 America/Los_Angeles", 600 | "web_order_line_item_id": "1000000035713887", 601 | "is_trial_period": "false" 602 | }, 603 | { 604 | "quantity": "1", 605 | "product_id": "testproduct", 606 | "transaction_id": "1000000318408761", 607 | "original_transaction_id": "1000000318012065", 608 | "purchase_date": "2017-07-25 09:06:19 Etc/GMT", 609 | "purchase_date_ms": "1500973579000", 610 | "purchase_date_pst": "2017-07-25 02:06:19 America/Los_Angeles", 611 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 612 | "original_purchase_date_ms": "1500884005000", 613 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 614 | "expires_date": "2017-07-25 09:11:19 Etc/GMT", 615 | "expires_date_ms": "1500973879000", 616 | "expires_date_pst": "2017-07-25 02:11:19 America/Los_Angeles", 617 | "web_order_line_item_id": "1000000035725079", 618 | "is_trial_period": "false" 619 | }, 620 | { 621 | "quantity": "1", 622 | "product_id": "testproduct", 623 | "transaction_id": "1000000318410476", 624 | "original_transaction_id": "1000000318012065", 625 | "purchase_date": "2017-07-25 09:11:19 Etc/GMT", 626 | "purchase_date_ms": "1500973879000", 627 | "purchase_date_pst": "2017-07-25 02:11:19 America/Los_Angeles", 628 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 629 | "original_purchase_date_ms": "1500884005000", 630 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 631 | "expires_date": "2017-07-25 09:16:19 Etc/GMT", 632 | "expires_date_ms": "1500974179000", 633 | "expires_date_pst": "2017-07-25 02:16:19 America/Los_Angeles", 634 | "web_order_line_item_id": "1000000035725139", 635 | "is_trial_period": "false" 636 | }, 637 | { 638 | "quantity": "1", 639 | "product_id": "testproduct", 640 | "transaction_id": "1000000318413351", 641 | "original_transaction_id": "1000000318012065", 642 | "purchase_date": "2017-07-25 09:16:19 Etc/GMT", 643 | "purchase_date_ms": "1500974179000", 644 | "purchase_date_pst": "2017-07-25 02:16:19 America/Los_Angeles", 645 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 646 | "original_purchase_date_ms": "1500884005000", 647 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 648 | "expires_date": "2017-07-25 09:21:19 Etc/GMT", 649 | "expires_date_ms": "1500974479000", 650 | "expires_date_pst": "2017-07-25 02:21:19 America/Los_Angeles", 651 | "web_order_line_item_id": "1000000035725196", 652 | "is_trial_period": "false" 653 | }, 654 | { 655 | "quantity": "1", 656 | "product_id": "testproduct", 657 | "transaction_id": "1000000318417975", 658 | "original_transaction_id": "1000000318012065", 659 | "purchase_date": "2017-07-25 09:23:30 Etc/GMT", 660 | "purchase_date_ms": "1500974610000", 661 | "purchase_date_pst": "2017-07-25 02:23:30 America/Los_Angeles", 662 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 663 | "original_purchase_date_ms": "1500884005000", 664 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 665 | "expires_date": "2017-07-25 09:28:30 Etc/GMT", 666 | "expires_date_ms": "1500974910000", 667 | "expires_date_pst": "2017-07-25 02:28:30 America/Los_Angeles", 668 | "web_order_line_item_id": "1000000035725250", 669 | "is_trial_period": "false" 670 | }, 671 | { 672 | "quantity": "1", 673 | "product_id": "testproduct", 674 | "transaction_id": "1000000318420598", 675 | "original_transaction_id": "1000000318012065", 676 | "purchase_date": "2017-07-25 09:28:30 Etc/GMT", 677 | "purchase_date_ms": "1500974910000", 678 | "purchase_date_pst": "2017-07-25 02:28:30 America/Los_Angeles", 679 | "original_purchase_date": "2017-07-24 08:13:25 Etc/GMT", 680 | "original_purchase_date_ms": "1500884005000", 681 | "original_purchase_date_pst": "2017-07-24 01:13:25 America/Los_Angeles", 682 | "expires_date": "2017-07-25 09:33:30 Etc/GMT", 683 | "expires_date_ms": "1500975210000", 684 | "expires_date_pst": "2017-07-25 02:33:30 America/Los_Angeles", 685 | "web_order_line_item_id": "1000000035725368", 686 | "is_trial_period": "false" 687 | } 688 | ], 689 | "latest_receipt": "DUMMY_RECEIPT:_A_BASE64_ENCODED_RECEIPT_DATA_WILL_BE_HERE", 690 | "pending_renewal_info": [ 691 | { 692 | "expiration_intent": "1", 693 | "auto_renew_product_id": "testproduct", 694 | "is_in_billing_retry_period": "0", 695 | "product_id": "testproduct", 696 | "auto_renew_status": "0" 697 | } 698 | ] 699 | }''') 700 | 701 | 702 | @pytest.fixture(scope='session') 703 | def itunes_autorenew_response3(): 704 | """Contributed by François Dupayrat @FrancoisDupayrat""" 705 | return json.loads(r'''{ 706 | "auto_renew_status": 1, 707 | "status": 0, 708 | "auto_renew_product_id": "******************************", 709 | "receipt":{ 710 | "original_purchase_date_pst":"2017-06-28 07:31:51 America/Los_Angeles", 711 | "unique_identifier":"******************************", 712 | "original_transaction_id":"******************************", 713 | "expires_date":"1506524970000", 714 | "transaction_id":"******************************", 715 | "quantity":"1", 716 | "product_id":"******************************", 717 | "item_id":"******************************", 718 | "bid":"******************************", 719 | "unique_vendor_identifier":"******************************", 720 | "web_order_line_item_id":"******************************", 721 | "bvrs":"1.1.6", 722 | "expires_date_formatted":"2017-09-27 15:09:30 Etc/GMT", 723 | "purchase_date":"2017-09-27 15:04:30 Etc/GMT", 724 | "purchase_date_ms":"1506524670000", 725 | "expires_date_formatted_pst":"2017-09-27 08:09:30 America/Los_Angeles", 726 | "purchase_date_pst":"2017-09-27 08:04:30 America/Los_Angeles", 727 | "original_purchase_date":"2017-06-28 14:31:51 Etc/GMT", 728 | "original_purchase_date_ms":"1498660311000" 729 | }, 730 | "latest_receipt_info":{ 731 | "original_purchase_date_pst":"2017-06-28 07:31:51 America/Los_Angeles", 732 | "unique_identifier":"******************************", 733 | "original_transaction_id":"******************************", 734 | "expires_date":"******************************", 735 | "transaction_id":"******************************", 736 | "quantity":"1", 737 | "product_id":"******************************", 738 | "item_id":"******************************", 739 | "bid":"******************************", 740 | "unique_vendor_identifier":"******************************", 741 | "web_order_line_item_id":"******************************", 742 | "bvrs":"1.1.6", 743 | "expires_date_formatted":"2017-09-27 15:09:30 Etc/GMT", 744 | "purchase_date":"2017-09-27 15:04:30 Etc/GMT", 745 | "purchase_date_ms":"1506524670000", 746 | "expires_date_formatted_pst":"2017-09-27 08:09:30 America/Los_Angeles", 747 | "purchase_date_pst":"2017-09-27 08:04:30 America/Los_Angeles", 748 | "original_purchase_date":"2017-06-28 14:31:51 Etc/GMT", 749 | "original_purchase_date_ms":"1498660311000" 750 | }, 751 | "latest_receipt":"******************************" 752 | }''') 753 | 754 | 755 | @pytest.fixture(params=[ 756 | lazy_fixture('itunes_autorenew_response1'), 757 | lazy_fixture('itunes_autorenew_response2'), 758 | lazy_fixture('itunes_autorenew_response3'), 759 | ]) 760 | def itunes_autorenew_response(request): 761 | return request.param 762 | --------------------------------------------------------------------------------