From bbe73288df58229e18dabd16b37c6f55858ed7e8 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 12:38:57 +0000 Subject: [PATCH 01/30] Fix test_list_subscription_payments_with_plan_id as the response does not return a 'plan' according to the Schema, this resulted in a KeyError. Please see documentation for the Schema https://developer.paddle.com/api-reference/subscription-api/payments/listpayments --- tests/test_subscription_payments.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_subscription_payments.py b/tests/test_subscription_payments.py index 7d52217..c41cb1f 100644 --- a/tests/test_subscription_payments.py +++ b/tests/test_subscription_payments.py @@ -60,9 +60,6 @@ def test_list_subscription_payments_with_plan_id(paddle_client): # NOQA: F811 skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 pytest.skip(skip_message) - for payment in response: - assert payment['plan'] == plan_id - def test_list_subscription_payments_with_from_to(paddle_client): # NOQA: F811 all_payments = paddle_client.list_subscription_payments() From 0377249387f5914e7f049ffe4c0c0cacaf284e0c Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 12:41:01 +0000 Subject: [PATCH 02/30] Fix test_preview_subscription_update to increment the quantity not the amount. The assert is then used to compare the expected amount (new_quantity * amount) with the next_payment amount returned from preview_update_subscription. --- tests/test_subscription_users.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_subscription_users.py b/tests/test_subscription_users.py index a98f07e..d8ac13e 100644 --- a/tests/test_subscription_users.py +++ b/tests/test_subscription_users.py @@ -338,9 +338,11 @@ def test_preview_subscription_update(mocker, paddle_client): # NOQA: F811 pytest.skip(skip_message) subscription_id = subscription_data['subscription_id'] + quantity = subscription_data['quantity'] amount = subscription_data['next_payment']['amount'] currency = subscription_data['next_payment']['currency'] - new_quantity = amount + 1 + new_quantity = quantity + 1 + expected_amount = new_quantity * amount response = paddle_client.preview_update_subscription( subscription_id=subscription_id, bill_immediately=True, @@ -354,7 +356,7 @@ def test_preview_subscription_update(mocker, paddle_client): # NOQA: F811 assert isinstance(response['immediate_payment']['date'], str) datetime.strptime(response['immediate_payment']['date'], '%Y-%m-%d') - assert response['next_payment']['amount'] == amount + assert response['next_payment']['amount'] == expected_amount assert response['next_payment']['currency'] == currency assert isinstance(response['next_payment']['date'], str) datetime.strptime(response['next_payment']['date'], '%Y-%m-%d') From 914c95efee179350e5bc7a48d42311c76f9fe50f Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 13:17:53 +0000 Subject: [PATCH 03/30] Fix test_list_transactions_product as it fails when the transaction has is_one_off=False. Documentation shows is_one_off can be True or False https://developer.paddle.com/api-reference/product-api/transactions/listtransactions --- tests/test_transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 8d8e917..cc82959 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -49,7 +49,7 @@ def test_list_transactions_product(paddle_client): # NOQA: F811 assert isinstance(product['passthrough'], str) assert isinstance(product['product_id'], int) assert product['is_subscription'] is False - assert product['is_one_off'] is True + assert isinstance(product['is_one_off'], bool) # assert isinstance(product['product']['product_id'], int) # assert isinstance(product['product']['status'], str) assert isinstance(product['user']['user_id'], int) From 23e6ef554f7c92aff597189001f6fb4efa336715 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 13:26:16 +0000 Subject: [PATCH 04/30] Fix test_list_transactions_product & test_list_transactions_checkout as the passthrough value can be null which raises an AssertionError. You may want to remove the check entirely instead of having an OR statement. https://developer.paddle.com/api-reference/product-api/transactions/listtransactions --- tests/test_transactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index cc82959..a53189c 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -46,7 +46,7 @@ def test_list_transactions_product(paddle_client): # NOQA: F811 assert isinstance(product['status'], str) assert isinstance(product['created_at'], str) datetime.strptime(product['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(product['passthrough'], str) + assert isinstance(product['passthrough'], str) or product['passthrough'] is None assert isinstance(product['product_id'], int) assert product['is_subscription'] is False assert isinstance(product['is_one_off'], bool) @@ -73,7 +73,7 @@ def test_list_transactions_checkout(paddle_client): # NOQA: F811 assert isinstance(checkout['status'], str) assert isinstance(checkout['created_at'], str) datetime.strptime(checkout['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(checkout['passthrough'], str) + assert isinstance(checkout['passthrough'], str) or checkout['passthrough'] is None assert isinstance(checkout['product_id'], int) assert isinstance(checkout['is_subscription'], bool) assert isinstance(checkout['is_one_off'], bool) From cb0fec612d27e53a2379916b619444a23bfbf23c Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 13:35:01 +0000 Subject: [PATCH 05/30] Fix test_list_transactions_subscription. Whilst searching for assertion checks on the passthrough value I found the list_transactions was not failing when a subscription was being passed (despite the passthrough returning null in postman). After investigating I found the plan_id (PADDLE_TEST_DEFAULT_PLAN_ID) was being passed as the subscription_id. In this instance the paddle API responds with {"success": true, "response": []} as the response is an empty list test never failed. Instead of passing a plan_id we should pass a subscription_id. --- tests/test_transactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index a53189c..b66300d 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -6,7 +6,7 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 # ToDo: Create plan when API exists for it here - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_PLAN_ID']) + subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) subscription_list = paddle_client.list_transactions( entity='subscription', entity_id=subscription_id, @@ -19,7 +19,7 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 assert isinstance(plan['status'], str) assert isinstance(plan['created_at'], str) datetime.strptime(plan['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(plan['passthrough'], str) + assert isinstance(plan['passthrough'], str) or plan['passthrough'] is None assert isinstance(plan['product_id'], int) assert plan['is_subscription'] is True assert plan['is_one_off'] is False From dcb6278487ee4d4cc73869107230d2b6c5c9b895 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 17:26:14 +0000 Subject: [PATCH 06/30] Fix test_list_transactions_subscription as plan.is_one_off can be true or false --- tests/test_transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index b66300d..8d58bea 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -22,7 +22,7 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 assert isinstance(plan['passthrough'], str) or plan['passthrough'] is None assert isinstance(plan['product_id'], int) assert plan['is_subscription'] is True - assert plan['is_one_off'] is False + assert isinstance(plan['is_one_off'], bool) assert isinstance(plan['subscription']['subscription_id'], int) assert isinstance(plan['subscription']['status'], str) assert isinstance(plan['user']['user_id'], int) From b2c1e4cad3812ddf6aa27204fb85d3a4bf4aa878 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 17:34:33 +0000 Subject: [PATCH 07/30] Fix test_create_one_off_charge_no_mock increase transaction amount to avoid error "amount is less than allowed minimum transaction amount" (Paddle error 186). Also fix assertion check on the amount as paddle returns the value to three decimal places. --- tests/test_one_off_charges.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_one_off_charges.py b/tests/test_one_off_charges.py index c7d6b44..9de0352 100644 --- a/tests/test_one_off_charges.py +++ b/tests/test_one_off_charges.py @@ -54,7 +54,7 @@ def test_create_one_off_charge(mocker, paddle_client): # NOQA: F811 @pytest.mark.skip() def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811 subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) - amount = 0.01 + amount = 1.00 response = paddle_client.create_one_off_charge( subscription_id=subscription_id, amount=amount, @@ -64,7 +64,7 @@ def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811 assert isinstance(response['currency'], str) assert isinstance(response['receipt_url'], str) assert response['subscription_id'] == subscription_id - assert response['amount'] == '%.2f' % round(amount, 2) + assert response['amount'] == '%.3f' % round(amount, 2) assert isinstance(response['payment_date'], str) datetime.strptime(response['payment_date'], '%Y-%m-%d') From 56128e48508797148ad0b3b5d0795073ecabf25e Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 17:52:32 +0000 Subject: [PATCH 08/30] Comment added to increase clarity around testing when unset_vendor_id is passed --- tests/test_user_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_user_history.py b/tests/test_user_history.py index b92784d..5bd935b 100644 --- a/tests/test_user_history.py +++ b/tests/test_user_history.py @@ -7,7 +7,7 @@ def test_get_user_history_with_vendor_id(unset_vendor_id): # NOQA: F811 email = 'test@example.com' - vendor_id = 11 + vendor_id = 11 # This will need to be manually entered paddle = PaddleClient(vendor_id=vendor_id) response = paddle.get_user_history(email=email, vendor_id=vendor_id) assert response == 'We\'ve sent details of your past transactions, licenses and downloads to you via email.' # NOQA: E501 @@ -32,7 +32,7 @@ def test_get_user_history_with_product_id(paddle_client): # NOQA: F811 def test_get_user_history_missing_vendoer_id_and_product_id(unset_vendor_id): # NOQA: F811, E501 email = 'test@example.com' - vendor_id = 11 + vendor_id = 11 # This will need to be manually entered paddle = PaddleClient(vendor_id=vendor_id) response = paddle.get_user_history(email=email) assert response == 'We\'ve sent details of your past transactions, licenses and downloads to you via email.' # NOQA: E501 From 3c8b04bf0c5b1eb6673958664d994cad85c472a0 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 18:43:15 +0000 Subject: [PATCH 09/30] Flake8 fix E501 line too long --- tests/test_transactions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 8d58bea..9c90402 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -19,7 +19,8 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 assert isinstance(plan['status'], str) assert isinstance(plan['created_at'], str) datetime.strptime(plan['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(plan['passthrough'], str) or plan['passthrough'] is None + assert isinstance(plan['passthrough'], str) \ + or plan['passthrough'] is None assert isinstance(plan['product_id'], int) assert plan['is_subscription'] is True assert isinstance(plan['is_one_off'], bool) @@ -46,7 +47,8 @@ def test_list_transactions_product(paddle_client): # NOQA: F811 assert isinstance(product['status'], str) assert isinstance(product['created_at'], str) datetime.strptime(product['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(product['passthrough'], str) or product['passthrough'] is None + assert isinstance(product['passthrough'], str) \ + or product['passthrough'] is None assert isinstance(product['product_id'], int) assert product['is_subscription'] is False assert isinstance(product['is_one_off'], bool) @@ -73,7 +75,8 @@ def test_list_transactions_checkout(paddle_client): # NOQA: F811 assert isinstance(checkout['status'], str) assert isinstance(checkout['created_at'], str) datetime.strptime(checkout['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(checkout['passthrough'], str) or checkout['passthrough'] is None + assert isinstance(checkout['passthrough'], str) \ + or checkout['passthrough'] is None assert isinstance(checkout['product_id'], int) assert isinstance(checkout['is_subscription'], bool) assert isinstance(checkout['is_one_off'], bool) From cba3b5f961c2f60e750748ea5deadc5f4b19e364 Mon Sep 17 00:00:00 2001 From: Joel Whitaker Date: Thu, 25 Feb 2021 18:57:41 +0000 Subject: [PATCH 10/30] Add support for Paddle Sandbox mode Add constructor parameter 'sandbox' to PaddleClient to enable sandbox mode If the parameter 'sandbox' is not passed in the constructor the option is loaded form the Environment variable 'PADDLE_SANDBOX' (default=False) Add get_environment_url(self, url) function to PaddleClient() which checks if sandbox has been enabled and if it had it prepends 'sandbox-' to the subdomain. Returns url Update PaddleClient request() to get the environment url after building and validating the url Update all tests to call paddle_client.get_environment_url() --- paddle/paddle.py | 14 +++++++++++++- tests/test_licenses.py | 1 + tests/test_one_off_charges.py | 1 + tests/test_pay_links.py | 1 + tests/test_plans.py | 1 + tests/test_product_payments.py | 1 + tests/test_subscription_users.py | 4 ++++ 7 files changed, 22 insertions(+), 1 deletion(-) diff --git a/paddle/paddle.py b/paddle/paddle.py index 05d680f..ea9e873 100644 --- a/paddle/paddle.py +++ b/paddle/paddle.py @@ -40,7 +40,8 @@ class PaddleClient(): called ``PADDLE_VENDOR_ID`` and ``PADDLE_API_KEY`` """ - def __init__(self, vendor_id: int = None, api_key: str = None): + def __init__(self, vendor_id: int = None, api_key: str = None, + sandbox: bool = None): if not vendor_id: try: vendor_id = int(os.environ['PADDLE_VENDOR_ID']) @@ -53,7 +54,11 @@ def __init__(self, vendor_id: int = None, api_key: str = None): api_key = os.environ['PADDLE_API_KEY'] except KeyError: raise ValueError('API key not set') + if sandbox is None: + # Load sandbox flag from environment if not set in the constructor + sandbox = os.getenv('PADDLE_SANDBOX', False) == 'True' + self.is_sandbox = sandbox is True self.checkout_v1 = 'https://checkout.paddle.com/api/1.0/' self.checkout_v2 = 'https://checkout.paddle.com/api/2.0/' self.checkout_v2_1 = 'https://vendors.paddle.com/api/2.1/' @@ -94,6 +99,7 @@ def request( warnings.warn(warning_message, RuntimeWarning) if 'paddle.com/api/' not in url: raise ValueError('URL "{0}" does not appear to be a Paddle API URL') # NOQA: E501 + url = self.get_environment_url(url) kwargs['url'] = url kwargs['method'] = method.upper() @@ -157,6 +163,12 @@ def post(self, url, **kwargs): kwargs['method'] = 'POST' return self.request(**kwargs) + def get_environment_url(self, url): + if self.is_sandbox: + url = url.replace("://", "://sandbox-", 1) + + return url + from ._order_information import get_order_details from ._user_history import get_user_history diff --git a/tests/test_licenses.py b/tests/test_licenses.py index fb79b1b..117c3c6 100644 --- a/tests/test_licenses.py +++ b/tests/test_licenses.py @@ -48,6 +48,7 @@ def test_generate_license_mocked(mocker, paddle_client): # NOQA: F811 } url = 'https://vendors.paddle.com/api/2.0/product/generate_license' + url = paddle_client.get_environment_url(url) method = 'POST' paddle_client.generate_license( diff --git a/tests/test_one_off_charges.py b/tests/test_one_off_charges.py index 9de0352..88a9f98 100644 --- a/tests/test_one_off_charges.py +++ b/tests/test_one_off_charges.py @@ -39,6 +39,7 @@ def test_create_one_off_charge(mocker, paddle_client): # NOQA: F811 url = 'https://vendors.paddle.com/api/2.0/subscription/{0}/charge'.format( subscription_id ) + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.create_one_off_charge( diff --git a/tests/test_pay_links.py b/tests/test_pay_links.py index 938b830..abb513d 100644 --- a/tests/test_pay_links.py +++ b/tests/test_pay_links.py @@ -92,6 +92,7 @@ def test_create_pay_link_mock(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/product/generate_license' + url = paddle_client.get_environment_url(url) method = 'POST' paddle_client.create_pay_link( diff --git a/tests/test_plans.py b/tests/test_plans.py index 48a6a48..eb7b24a 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -128,6 +128,7 @@ def test_create_plan_mock(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/subscription/plans_create' + url = paddle_client.get_environment_url(url) method = 'POST' paddle_client.create_plan( diff --git a/tests/test_product_payments.py b/tests/test_product_payments.py index 4f339fa..38f4ab1 100644 --- a/tests/test_product_payments.py +++ b/tests/test_product_payments.py @@ -36,6 +36,7 @@ def test_refund_product_payment(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/payment/refund' + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.refund_product_payment( diff --git a/tests/test_subscription_users.py b/tests/test_subscription_users.py index d8ac13e..f592c47 100644 --- a/tests/test_subscription_users.py +++ b/tests/test_subscription_users.py @@ -140,6 +140,7 @@ def test_cancel_subscription(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/subscription/users_cancel' + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.cancel_subscription( @@ -207,6 +208,7 @@ def test_update_subscription(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.update_subscription( @@ -294,6 +296,7 @@ def test_pause_subscription(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.pause_subscription(subscription_id=subscription_id) @@ -318,6 +321,7 @@ def test_resume_subscription(mocker, paddle_client): # NOQA: F811 'vendor_auth_code': os.environ['PADDLE_API_KEY'], } url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' + url = paddle_client.get_environment_url(url) method = 'POST' request = mocker.patch('paddle.paddle.requests.request') paddle_client.resume_subscription(subscription_id=subscription_id) From 00e33af73f82fcc9861b4b979ae81af6de18a866 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:28:16 +0000 Subject: [PATCH 11/30] Use error.match to stop double call for exception tests --- tests/test_coupons.py | 9 ++---- tests/test_one_off_charges.py | 16 ++++------- tests/test_order_information.py | 9 +++--- tests/test_paddle.py | 49 +++++++++++++++------------------ tests/test_prices.py | 9 ++---- tests/test_user_history.py | 9 +++--- 6 files changed, 40 insertions(+), 61 deletions(-) diff --git a/tests/test_coupons.py b/tests/test_coupons.py index 7eee722..efb05f8 100644 --- a/tests/test_coupons.py +++ b/tests/test_coupons.py @@ -58,13 +58,10 @@ def test_list_coupons(paddle_client, create_coupon): # NOQA: F811 def test_list_coupons_invalid_product(paddle_client): # NOQA: F811 product_id = 11 - paddle_error = 'Paddle error 108 - Unable to find requested product' - with pytest.raises(PaddleException): + with pytest.raises(PaddleException) as error: paddle_client.list_coupons(product_id=product_id) - try: - paddle_client.list_coupons(product_id=product_id) - except PaddleException as error: - assert str(error) == paddle_error + + error.match('Paddle error 108 - Unable to find requested product') def test_create_coupon(paddle_client): # NOQA: F811 diff --git a/tests/test_one_off_charges.py b/tests/test_one_off_charges.py index 88a9f98..ee8c85a 100644 --- a/tests/test_one_off_charges.py +++ b/tests/test_one_off_charges.py @@ -65,26 +65,20 @@ def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811 assert isinstance(response['currency'], str) assert isinstance(response['receipt_url'], str) assert response['subscription_id'] == subscription_id - assert response['amount'] == '%.3f' % round(amount, 2) + assert response['amount'] == '%.2f' % round(amount, 2) assert isinstance(response['payment_date'], str) datetime.strptime(response['payment_date'], '%Y-%m-%d') def test_create_one_off_charge_zero_ammount(paddle_client): # NOQA: F811 subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) - with pytest.raises(PaddleException): - paddle_client.create_one_off_charge( - subscription_id=subscription_id, - amount=0.0, - charge_name="test_create_one_off_charge" - ) - error = 'Paddle error 183 - Charges cannot be made with a negative amount' - try: + with pytest.raises(PaddleException) as error: paddle_client.create_one_off_charge( subscription_id=subscription_id, amount=0.0, charge_name="test_create_one_off_charge" ) - except PaddleException as e: - assert str(e) == error + + msg = 'Paddle error 183 - Charges cannot be made with a negative amount' + error.match(msg) diff --git a/tests/test_order_information.py b/tests/test_order_information.py index 609b373..e8db65a 100644 --- a/tests/test_order_information.py +++ b/tests/test_order_information.py @@ -19,9 +19,8 @@ def test_get_order_details(paddle_client): # NOQA: F811 def test_get_order_details_invalid_id(paddle_client): # NOQA: F811 - with pytest.raises(PaddleException): + with pytest.raises(PaddleException) as error: paddle_client.get_order_details(checkout_id='fake-id') - try: - paddle_client.get_order_details(checkout_id='fake-id') - except PaddleException as e: - assert str(e) == 'Paddle error 101 - Could not find a checkout matching this ID' # NOQA: E501 + + msg = 'Paddle error 101 - Could not find a checkout matching this ID' + error.match(msg) diff --git a/tests/test_paddle.py b/tests/test_paddle.py index ea659bf..0e8908b 100644 --- a/tests/test_paddle.py +++ b/tests/test_paddle.py @@ -47,45 +47,40 @@ def test_paddle_vendor_id_not_set(unset_vendor_id): def test_paddle_vendor_id_not_int(set_vendor_id_to_invalid): - with pytest.raises(ValueError): - PaddleClient(api_key='test') - try: + with pytest.raises(ValueError) as error: PaddleClient(api_key='test') - except ValueError as error: - assert str(error) == 'Vendor ID must be a number' + error.match('Vendor ID must be a number') -def test_paddle_api_key_not_set(unset_api_key): - with pytest.raises(ValueError): +def test_paddle_api_key_not_set(unset_vendor_id, unset_api_key): + with pytest.raises(ValueError) as error: PaddleClient(vendor_id=1) - try: - PaddleClient(vendor_id=1) - except ValueError as error: - assert str(error) == 'API key not set' + error.match('API key not set') + + +def test_sandbox(paddle_client): + with pytest.raises(PaddleException) as error: + paddle_client.post('https://sandbox-checkout.paddle.com/api/1.0/order') + + msg = 'HTTP error 405 - The method used for the Request is not allowed for the requested resource.' # NOQA: E501 + error.match(msg) def test_paddle_json_and_data(paddle_client): - with pytest.raises(ValueError): - paddle_client.get('anyurl', json={'a': 'b'}, data={'a': 'b'}) - try: + with pytest.raises(ValueError) as error: paddle_client.get('anyurl', json={'a': 'b'}, data={'a': 'b'}) - except ValueError as error: - assert str(error) == 'Please set either data or json not both' + error.match('Please set either data or json not both') def test_paddle_data_and_json(paddle_client): - with pytest.raises(PaddleException): + with pytest.raises(PaddleException) as error: paddle_client.get('/badurl') - try: - paddle_client.get('badurl') - except PaddleException as error: - assert str(error) == 'Paddle error 101 - Bad method call' + error.match('Paddle error 101 - Bad method call') def test_paddle_http_error(paddle_client): - with pytest.raises(PaddleException): - paddle_client.post('https://checkout.paddle.com/api/1.0/order') - try: - paddle_client.post('https://checkout.paddle.com/api/1.0/order') - except PaddleException as error: - assert str(error) == 'HTTP error 405 - The method used for the Request is not allowed for the requested resource.' # NOQA: E501 + with pytest.raises(PaddleException) as error: + paddle_client.post('https://sandbox-checkout.paddle.com/api/1.0/order') + + message = 'HTTP error 405 - The method used for the Request is not allowed for the requested resource.' # NOQA: E501 + error.match(message) diff --git a/tests/test_prices.py b/tests/test_prices.py index 604f83e..6492c43 100644 --- a/tests/test_prices.py +++ b/tests/test_prices.py @@ -39,17 +39,12 @@ def test_get_prices_invalid_customer_country(paddle_client): # NOQA: F811 bad_country = '00' value_error = 'Country code "{0}" is not valid'.format(bad_country) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as error: paddle_client.get_prices( product_ids=[product_id], customer_country=bad_country, ) - try: - paddle_client.get_prices( - product_ids=[product_id], customer_country=bad_country, - ) - except ValueError as error: - assert str(error) == value_error + error.match(value_error) def test_get_prices_with_customer_ip(paddle_client): # NOQA: F811 diff --git a/tests/test_user_history.py b/tests/test_user_history.py index 5bd935b..37e9b73 100644 --- a/tests/test_user_history.py +++ b/tests/test_user_history.py @@ -22,12 +22,11 @@ def test_get_user_history_with_vendor_id_env_var(paddle_client): # NOQA: F811 def test_get_user_history_with_product_id(paddle_client): # NOQA: F811 email = 'test@example.com' product_id = 1 - with pytest.raises(PaddleException): + with pytest.raises(PaddleException) as error: paddle_client.get_user_history(email=email, product_id=product_id) - try: - paddle_client.get_user_history(email=email, product_id=product_id) - except PaddleException as error: - assert str(error) == 'Paddle error 110 - We were unable to find any past transactions, licenses or downloads matching that email address.' # NOQA: E501 + + msg = 'Paddle error 110 - We were unable to find any past transactions, licenses or downloads matching that email address.' # NOQA: E501 + error.match(msg) def test_get_user_history_missing_vendoer_id_and_product_id(unset_vendor_id): # NOQA: F811, E501 From 735414ebbb35f300748fa28d23393681ff68efe8 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:29:46 +0000 Subject: [PATCH 12/30] Remove preview_subscription_update as Paddle has removed this endpoint --- README.md | 7 +------ docs/api_reference.rst | 1 - paddle/_subscription_users.py | 33 --------------------------------- paddle/paddle.py | 3 --- 4 files changed, 1 insertion(+), 43 deletions(-) diff --git a/README.md b/README.md index 095eda9..3f50075 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,6 @@ See [`Usage`](#usage) below for quick examples. * [List Subscription Users](https://developer.paddle.com/api-reference/subscription-api/subscription-users/listusers) * [Cancel Subscription](https://developer.paddle.com/api-reference/subscription-api/subscription-users/canceluser) * [Update Subscription](https://developer.paddle.com/api-reference/subscription-api/subscription-users/updateuser) -* [Preview Subscription Update](https://developer.paddle.com/api-reference/subscription-api/subscription-users/previewupdate) * [Add Modifier](https://developer.paddle.com/api-reference/subscription-api/modifiers/createmodifier) * [Delete Modifier](https://developer.paddle.com/api-reference/subscription-api/modifiers/deletemodifier) * [List Modifiers](https://developer.paddle.com/api-reference/subscription-api/modifiers/listmodifiers) @@ -93,6 +92,7 @@ See [`Usage`](#usage) below for quick examples. **Alert API** * [Get Webhook History](https://developer.paddle.com/api-reference/alert-api/webhooks/webhooks) + ### Usage See the [API Reference in the docs](https://paddle-client.readthedocs.io/en/latest/api_reference.html) for full usage with param are return details. @@ -164,11 +164,6 @@ paddle.update_subscription( ) paddle.pause_subscription(subscription_id=1234) paddle.resume_subscription(subscription_id=1234) -paddle.preview_update_subscription( - subscription_id=123, - bill_immediately=True, - quantity=101, -) paddle.add_modifier(subscription_id=1234, modifier_amount=10.5) paddle.delete_modifier(modifier_id=10) paddle.list_modifiers() diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 565823c..ec96d3d 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -36,7 +36,6 @@ As listed in the `Paddle API Reference ` - :meth:`Cancel Subscription` - :meth:`Update Subscription` - (Including :meth:`Pause Subscription` and :meth:`Resume Subscription`) -- :meth:`Preview Subscription Update` - :meth:`Add Modifier` - :meth:`Delete Modifier` - :meth:`List Modifiers` diff --git a/paddle/_subscription_users.py b/paddle/_subscription_users.py index 6988569..01c93ea 100644 --- a/paddle/_subscription_users.py +++ b/paddle/_subscription_users.py @@ -104,36 +104,3 @@ def resume_subscription(self, subscription_id: int) -> dict: sent when pausing/resuming subscriptions """ # NOQA: E501 return self.update_subscription(subscription_id=subscription_id, pause=False) # NOQA: E501 - - -def preview_subscription_update( - self, - subscription_id: int, - quantity: int = None, - bill_immediately: bool = None, - prorate: bool = None, - plan_id: int = None, - currency: str = None, - recurring_price: float = None, - keep_modifiers: bool = None, -) -> dict: - """ - `Preview Subscription Update Paddle docs `_ - """ # NOQA: E501 - url = urljoin(self.vendors_v2, 'subscription/preview_update') - - currency_codes = ['USD', 'GBP', 'EUR'] - if currency and currency not in currency_codes: - raise ValueError('currency must be one of {0}'.format(', '.join(currency_codes))) # NOQA: E501 - - json = { - 'subscription_id': subscription_id, - 'quantity': quantity, - 'currency': currency, - 'recurring_price': recurring_price, - 'bill_immediately': bill_immediately, - 'plan_id': plan_id, - 'prorate': prorate, - 'keep_modifiers': keep_modifiers, - } # type: PaddleJsonType - return self.post(url=url, json=json) diff --git a/paddle/paddle.py b/paddle/paddle.py index ea9e873..81f29dc 100644 --- a/paddle/paddle.py +++ b/paddle/paddle.py @@ -200,9 +200,6 @@ def get_environment_url(self, url): from ._subscription_users import update_subscription from ._subscription_users import pause_subscription from ._subscription_users import resume_subscription - from ._subscription_users import preview_subscription_update - # Alias to better match update_subscription - from ._subscription_users import preview_subscription_update as preview_update_subscription # NOQA: E501 from ._modifiers import add_modifier from ._modifiers import delete_modifier From f1978b47580e6908a977e96fcd4200aa8124e3e0 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:30:27 +0000 Subject: [PATCH 13/30] Fixup issues with create_pay_link --- paddle/_licenses.py | 1 - paddle/_pay_links.py | 17 +++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/paddle/_licenses.py b/paddle/_licenses.py index eac5f12..aad3975 100644 --- a/paddle/_licenses.py +++ b/paddle/_licenses.py @@ -14,7 +14,6 @@ def generate_license( expires_at: DatetimeType = None, ) -> dict: """ - `Generate License Paddle docs `_ """ # NOQA: E501 url = urljoin(self.vendors_v2, 'product/generate_license') diff --git a/paddle/_pay_links.py b/paddle/_pay_links.py index 941bbad..c39e5ca 100644 --- a/paddle/_pay_links.py +++ b/paddle/_pay_links.py @@ -27,7 +27,7 @@ def create_pay_link( expires: DatetimeType = None, affiliates: List[str] = None, recurring_affiliate_limit: int = None, - marketing_consent: str = None, + marketing_consent: bool = None, customer_email: str = None, customer_country: str = None, customer_postcode: str = None, @@ -47,13 +47,16 @@ def create_pay_link( Paddle error 108 - Unable to find requested product - Even though the docs states: "If no product_id is set, custom non-subscription product checkouts can be generated instead by specifying title, webhook_url and prices." + + Sending an invalid coupon code will result in the request failing with + "Paddle error 101 - Bad method call" + """ # NOQA: E501 - url = urljoin(self.vendors_v2, 'product/generate_license') + url = urljoin(self.vendors_v2, 'product/generate_pay_link') if not product_id: if not title: @@ -62,14 +65,16 @@ def create_pay_link( raise ValueError('webhook_url must be set if product_id is not set') # NOQA: E501 if recurring_prices: raise ValueError('recurring_prices can only be set if product_id is set to a subsciption') # NOQA: E501 + if webhook_url and product_id: + raise ValueError('product_id and webhook_url cannot both be set') # NOQA: E501 if customer_country: if customer_country not in supported_countries.keys(): error = 'Country code "{0}" is not valid'.format(customer_country) raise ValueError(error) if customer_country in countries_requiring_postcode and not customer_postcode: # NOQA: E501 error = ('customer_postcode must be set for {0} when ' - 'customer_country is set'.format(vat_country)) - raise ValueError(error) + 'customer_country is set') + raise ValueError(error.format(customer_country)) if vat_number: if not vat_company_name: @@ -103,7 +108,7 @@ def create_pay_link( 'quantity': quantity, 'affiliates': affiliates, 'recurring_affiliate_limit': recurring_affiliate_limit, - 'marketing_consent': marketing_consent, + 'marketing_consent': '1' if marketing_consent else '0', 'customer_email': customer_email, 'customer_country': customer_country, 'customer_postcode': customer_postcode, From 9d224dbc258b15fc094a75358b97d34f5fa762e5 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:31:11 +0000 Subject: [PATCH 14/30] Set sandbox urls on init --- paddle/_subscription_payments.py | 2 +- paddle/paddle.py | 44 ++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/paddle/_subscription_payments.py b/paddle/_subscription_payments.py index 0a5394c..99d7c82 100644 --- a/paddle/_subscription_payments.py +++ b/paddle/_subscription_payments.py @@ -2,7 +2,7 @@ from typing import List, Union from urllib.parse import urljoin -from .types import DateType, PaddleJsonType +from .types import DateType, PaddleJsonType # NOQA: F401 from .validators import validate_date log = logging.getLogger(__name__) diff --git a/paddle/paddle.py b/paddle/paddle.py index 81f29dc..50ea0fa 100644 --- a/paddle/paddle.py +++ b/paddle/paddle.py @@ -1,6 +1,7 @@ import logging import os import warnings +from distutils.util import strtobool from urllib.parse import urljoin import requests @@ -40,8 +41,12 @@ class PaddleClient(): called ``PADDLE_VENDOR_ID`` and ``PADDLE_API_KEY`` """ - def __init__(self, vendor_id: int = None, api_key: str = None, - sandbox: bool = None): + def __init__( + self, + vendor_id: int = None, + api_key: str = None, + sandbox: bool = None, + ): if not vendor_id: try: vendor_id = int(os.environ['PADDLE_VENDOR_ID']) @@ -55,14 +60,18 @@ def __init__(self, vendor_id: int = None, api_key: str = None, except KeyError: raise ValueError('API key not set') if sandbox is None: - # Load sandbox flag from environment if not set in the constructor - sandbox = os.getenv('PADDLE_SANDBOX', False) == 'True' + sandbox = bool(strtobool(os.getenv('PADDLE_SANDBOX', 'False'))) - self.is_sandbox = sandbox is True + self.sandbox = sandbox self.checkout_v1 = 'https://checkout.paddle.com/api/1.0/' self.checkout_v2 = 'https://checkout.paddle.com/api/2.0/' self.checkout_v2_1 = 'https://vendors.paddle.com/api/2.1/' self.vendors_v2 = 'https://vendors.paddle.com/api/2.0/' + if self.sandbox: + self.checkout_v1 = 'https://sandbox-checkout.paddle.com/api/1.0/' + self.checkout_v2 = 'https://sandbox-checkout.paddle.com/api/2.0/' + self.checkout_v2_1 = 'https://sandbox-vendors.paddle.com/api/2.1/' + self.vendors_v2 = 'https://sandbox-vendors.paddle.com/api/2.0/' self.default_url = self.vendors_v2 self.vendor_id = vendor_id @@ -72,12 +81,17 @@ def __init__(self, vendor_id: int = None, api_key: str = None, 'vendor_auth_code': self.api_key, } - self.url_warning = ( + self.relative_url_warning = ( 'Paddle recieved a relative URL so it will attempt to join it to ' '{0} as it is the Paddle URL with the most endpoints. The full ' 'URL that will be used is: {1} - You should specifiy the full URL ' 'as this default URL may change in the future.' ) + self.sandbox_url_warning = ( + 'PaddleClient is configured in sandbox mode but the URL provided ' + 'does not point to a sandbox subdomain. The URL will be converted ' + 'to use the Paddle sandbox ({0})' + ) def request( self, @@ -95,11 +109,15 @@ def request( if url.startswith('/'): url = url[1:] url = urljoin(self.default_url, url) - warning_message = self.url_warning.format(self.default_url, url) + warning_message = self.relative_url_warning.format(self.default_url, url) # NOQA: E501 warnings.warn(warning_message, RuntimeWarning) - if 'paddle.com/api/' not in url: - raise ValueError('URL "{0}" does not appear to be a Paddle API URL') # NOQA: E501 - url = self.get_environment_url(url) + elif 'paddle.com/api/' not in url: + error = 'URL does not appear to be a Paddle API URL - {0}' + raise ValueError(error.format(url)) + elif self.sandbox and '://sandbox-' not in url: + url = url.replace('://', '://sandbox-', 1) + warnings.warn(self.sandbox_url_warning.format(url), RuntimeWarning) + kwargs['url'] = url kwargs['method'] = method.upper() @@ -163,12 +181,6 @@ def post(self, url, **kwargs): kwargs['method'] = 'POST' return self.request(**kwargs) - def get_environment_url(self, url): - if self.is_sandbox: - url = url.replace("://", "://sandbox-", 1) - - return url - from ._order_information import get_order_details from ._user_history import get_user_history From 2de86312f1e32141cead5ceafe41a65da5db064d Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:32:49 +0000 Subject: [PATCH 15/30] Fixup generate license tests --- paddle/paddle.py | 4 +-- tests/test_licenses.py | 64 +++++++++--------------------------------- 2 files changed, 16 insertions(+), 52 deletions(-) diff --git a/paddle/paddle.py b/paddle/paddle.py index 50ea0fa..898b0ae 100644 --- a/paddle/paddle.py +++ b/paddle/paddle.py @@ -194,9 +194,9 @@ def post(self, url, **kwargs): from ._products import list_products - # from ._licenses import generate_license + from ._licenses import generate_license - # from ._pay_links import create_pay_link + from ._pay_links import create_pay_link from ._transactions import list_transactions diff --git a/tests/test_licenses.py b/tests/test_licenses.py index 117c3c6..6d7bb15 100644 --- a/tests/test_licenses.py +++ b/tests/test_licenses.py @@ -1,20 +1,21 @@ -import os from datetime import datetime -import pytest +from .fixtures import get_product, paddle_client # NOQA: F401 -from .test_paddle import paddle_client # NOQA: F401 - - -@pytest.mark.manual_cleanup -def test_generate_license(paddle_client): # NOQA: F811 - generate_license = getattr(paddle_client, 'generate_license', None) - if not generate_license or not callable(generate_license): - pytest.skip('paddle.generate_license does not exist') +def test_generate_license(paddle_client, get_product): # NOQA: F811 + """ + The product used must have the Fulfillment Method: `Paddle License` + or his test will fail. + The list_products endpoint / get_product fixture does not include the + fulfillment method, it can only be checked manually at: + https://sandbox-vendors.paddle.com/products + """ # ToDo: Create product when API exists for it here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - allowed_uses = 1 + + # Note: This product must be a + product_id = get_product['id'] + allowed_uses = 999 expires_at = datetime.now().strftime('%Y-%m-%d') response = paddle_client.generate_license( @@ -22,42 +23,5 @@ def test_generate_license(paddle_client): # NOQA: F811 allowed_uses=allowed_uses, expires_at=expires_at, ) - assert 'license_code' in response + assert isinstance(response['license_code'], str) assert response['expires_at'] == expires_at - - -def test_generate_license_mocked(mocker, paddle_client): # NOQA: F811 - """ - Mock test as the above test is not run by tox due to manual_cleanup mark - """ - generate_license = getattr(paddle_client, 'generate_license', None) - if not generate_license or not callable(generate_license): - pytest.skip('paddle.generate_license does not exist') - - request = mocker.patch('paddle.paddle.requests.request') - - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - allowed_uses = 1 - expires_at = datetime.now() - json = { - 'product_id': product_id, - 'allowed_uses': allowed_uses, - 'expires_at': expires_at.strftime('%Y-%m-%d'), - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - - } - url = 'https://vendors.paddle.com/api/2.0/product/generate_license' - url = paddle_client.get_environment_url(url) - method = 'POST' - - paddle_client.generate_license( - product_id=product_id, - allowed_uses=allowed_uses, - expires_at=expires_at, - ) - request.assert_called_once_with( - url=url, - json=json, - method=method, - ) From 95d5792ab7d63debdebf920e712ee14b3d6d08f2 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:35:00 +0000 Subject: [PATCH 16/30] Move all tests to the sandbox env and create fixtures for data --- tests/create_subscription.html | 64 +++++++++++ tests/data/fake-application.txt | 1 + tests/fixtures.py | 183 ++++++++++++++++++++++++++++++++ tests/test_paddle.py | 124 +++++++++++++++++----- 4 files changed, 344 insertions(+), 28 deletions(-) create mode 100644 tests/create_subscription.html create mode 100644 tests/data/fake-application.txt create mode 100644 tests/fixtures.py diff --git a/tests/create_subscription.html b/tests/create_subscription.html new file mode 100644 index 0000000..1d24dc1 --- /dev/null +++ b/tests/create_subscription.html @@ -0,0 +1,64 @@ + + + + + + paddle-client - create subscription + + + + + +
+

Create test subscription

+

+ Use the sandbox card details

+
+      
+        Card number: 4242 4242 4242 4242
+        Cardholder name: Test Name
+        Expiration Date: 12/2039
+        Security Code: 111
+      
+    
+ + Buy Now! + + + +
+ + + + + diff --git a/tests/data/fake-application.txt b/tests/data/fake-application.txt new file mode 100644 index 0000000..49cd3e4 --- /dev/null +++ b/tests/data/fake-application.txt @@ -0,0 +1 @@ +https://github.com/paddle-python/paddle-client/ diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..f3a05bc --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,183 @@ +from datetime import datetime + +import pytest + +from paddle import PaddleClient, PaddleException + + +@pytest.fixture(scope='session') +def paddle_client(): + """ + Paddle client details are for the paddle-client@pkgdeploy.com sandbox + account. If these details do not work or you require access to this + account please raise a GitHub issue. + """ + paddle = PaddleClient( + vendor_id=1468, + api_key="2ab9a716510da54fefa9b708b0e5b94f8145dcd8ba983bfab3", + sandbox=True, + ) + return paddle + + +@pytest.fixture(scope='session') +def create_plan(paddle_client): + """ + https://sandbox-vendors.paddle.com/subscriptions/plans + + Returns the plan ID + """ + plan_name = 'paddle-python-fixture-create_plan' + + plans = paddle_client.list_plans() + if plans: + for plan in plans: + if plan['name'] == plan_name: + return plan + + response = paddle_client.create_plan( + plan_name=plan_name, + plan_trial_days=0, + plan_length=1, + plan_type='day', + main_currency_code='USD', + initial_price_usd=1.00, + initial_price_gbp=1.00, + initial_price_eur=1.00, + recurring_price_usd=1.00, + recurring_price_gbp=1.00, + recurring_price_eur=1.00, + ) + return response + + +@pytest.fixture(scope='session') +def get_product(paddle_client): + """ + https://sandbox-vendors.paddle.com/products + + Returns a dict of product info + """ + response = paddle_client.list_products() + found_product = None + for product in response.get('products', []): + if product['name'] == 'test-product': + found_product = product + + if not found_product: + message = ( + 'No product found with name "test-product". As there is no Paddle ' + 'API endpoint to create products please create a GitHub issue to ' + 'let the maintainers of this package aware that no products exist ' + 'in the sandbox account' + ) + pytest.fail(message) + return + + return found_product + + +@pytest.fixture(scope='session') +def get_subscription(paddle_client, create_plan): + """ + https://sandbox-vendors.paddle.com/subscriptions/customers + + A subscription is record of a plan being purchase by a customer. + This can only be done via the Paddle Checkout JS, it is not possible to + create a subscirption via the Paddle API. + + Returns a dict with subscription info + """ + subscriptions = paddle_client.list_subscription_users() + if subscriptions: + return subscriptions[-1] + + fail_message = ( + 'It was not possible to find a checkout in the paddle sandbox account ' + 'which is required for this test. Please follow the instructions under' + ' `Creating subscription` in CONTRIBUTING.md using the plan ID: {0}' + ) + plan_id = create_plan['id'] + pytest.fail(fail_message.format(plan_id)) + return + + +@pytest.fixture(scope='session') +def get_checkout(paddle_client, get_subscription): + """ + https://sandbox-vendors.paddle.com/orders + + A checkout is a record of a single payment by a customer. + This can only be done via the Paddle Checkout JS, it is not possible to + create a checkout via the Paddle API. + + Returns a checkout ID + """ + subscription_list = paddle_client.list_transactions( + entity='subscription', + entity_id=get_subscription['subscription_id'], + ) + for subscription in subscription_list: + return subscription['checkout_id'] + + fail_message = ( + 'It was not possible to find a checkout in the paddle sandbox account ' + 'which is required for this test. Please follow the instructions under' + ' `Creating a subscription` in CONTRIBUTING.md using the plan ID: {0}' + ) + pytest.fail(fail_message.format(get_subscription['plan_id'])) + return + + +@pytest.fixture() +def create_coupon(paddle_client, get_product): + product_id = get_product['id'] + currency = 'USD' + now = datetime.now().isoformat() + response = paddle_client.create_coupon( + coupon_type='product', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency=currency, + product_ids=[product_id], + coupon_code='paddle-python-create_coupon_fixture-{0}'.format(now), + description='Test coupon created by paddle-python create_coupon_fixture', # NOQA: E501 + expires=datetime.today(), + minimum_threshold=9999, + group='paddle-python', + ) + coupon_code = response['coupon_codes'][0] + yield coupon_code, product_id + + try: + paddle_client.delete_coupon( + coupon_code=coupon_code, product_id=product_id + ) + except PaddleException as error: + valid_error = 'Paddle error 135 - Unable to find requested coupon' + if str(error) != valid_error: + raise + + +@pytest.fixture() +def create_modifier(paddle_client, get_subscription): # NOQA: F811 + subscription_id = get_subscription['subscription_id'] + response = paddle_client.add_modifier( + subscription_id=subscription_id, + modifier_amount=0.01, + modifier_recurring=True, + modifier_description='test_modifier_fixture_modifier_description', + ) + modifier_id = response['modifier_id'] + subscription_id = response['subscription_id'] + + yield modifier_id, subscription_id + + try: + paddle_client.delete_modifier(modifier_id=modifier_id) + except PaddleException as error: + valid_error = 'Paddle error 123 - Unable to find requested modifier' + if str(error) != valid_error: + raise diff --git a/tests/test_paddle.py b/tests/test_paddle.py index 0e8908b..7174788 100644 --- a/tests/test_paddle.py +++ b/tests/test_paddle.py @@ -1,21 +1,29 @@ +import os + import pytest from paddle import PaddleClient, PaddleException +from .fixtures import paddle_client # NOQA: F401 + class BadPaddleDataWarning(UserWarning): pass -@pytest.fixture(scope='session') -def paddle_client(): - paddle = PaddleClient() - return paddle +@pytest.fixture() +def set_vendor_id(monkeypatch): + monkeypatch.setenv('PADDLE_VENDOR_ID', '1234') @pytest.fixture() -def unset_vendor_id(monkeypatch): - monkeypatch.delenv('PADDLE_VENDOR_ID', raising=False) +def set_api_key(monkeypatch): + monkeypatch.setenv('PADDLE_API_KEY', 'abcdefghijklmnopqrstuvwxyz') + + +@pytest.fixture() +def set_sandbox(monkeypatch): + monkeypatch.setenv('PADDLE_SANDBOX', 'true') @pytest.fixture() @@ -23,42 +31,72 @@ def set_vendor_id_to_invalid(monkeypatch): monkeypatch.setenv('PADDLE_VENDOR_ID', 'Not an int') +@pytest.fixture() +def unset_vendor_id(monkeypatch): + monkeypatch.delenv('PADDLE_VENDOR_ID', raising=False) + + @pytest.fixture() def unset_api_key(monkeypatch): monkeypatch.delenv('PADDLE_API_KEY', raising=False) -def test_paddle__manual_vendor_id_and_api_key(unset_vendor_id, unset_api_key): - with pytest.raises(ValueError): - PaddleClient(api_key='test') - try: - PaddleClient(api_key='test') - except ValueError as error: - assert str(error) == 'Vendor ID not set' +def test_init_ignore_env_vars(set_vendor_id, set_api_key, set_sandbox): + vendor_id = 9999 + api_key = 'not-env-var' + sandbox = False + client = PaddleClient( + vendor_id=vendor_id, + api_key=api_key, + sandbox=sandbox, + ) + assert client.vendor_id == vendor_id + assert client.api_key == api_key + assert client.sandbox == sandbox -def test_paddle_vendor_id_not_set(unset_vendor_id): - with pytest.raises(ValueError): - PaddleClient(api_key='test') - try: +def test_vendor_id_env_var(set_vendor_id): + client = PaddleClient(api_key='test') + assert client.vendor_id == int(os.environ['PADDLE_VENDOR_ID']) + + +def test_api_key_env_var(set_api_key): + client = PaddleClient(vendor_id=1) + assert client.api_key == os.environ['PADDLE_API_KEY'] + + +def test_sandbox_env_var(set_sandbox): + client = PaddleClient(vendor_id=1, api_key='test') + assert client.sandbox is True + + +def test_sandbox_urls(paddle_client): # NOQA: F811 + assert paddle_client.checkout_v1.startswith('https://sandbox-') + assert paddle_client.checkout_v2.startswith('https://sandbox-') + assert paddle_client.checkout_v2_1.startswith('https://sandbox-') + assert paddle_client.vendors_v2.startswith('https://sandbox-') + assert paddle_client.default_url == paddle_client.vendors_v2 + + +def test_vendor_id_not_set(unset_vendor_id, unset_api_key): + with pytest.raises(ValueError) as error: PaddleClient(api_key='test') - except ValueError as error: - assert str(error) == 'Vendor ID not set' + error.match('Vendor ID not set') -def test_paddle_vendor_id_not_int(set_vendor_id_to_invalid): +def test_vendor_id_not_int(set_vendor_id_to_invalid): with pytest.raises(ValueError) as error: PaddleClient(api_key='test') error.match('Vendor ID must be a number') -def test_paddle_api_key_not_set(unset_vendor_id, unset_api_key): +def test_api_key_not_set(unset_vendor_id, unset_api_key): with pytest.raises(ValueError) as error: PaddleClient(vendor_id=1) error.match('API key not set') -def test_sandbox(paddle_client): +def test_sandbox(paddle_client): # NOQA: F811 with pytest.raises(PaddleException) as error: paddle_client.post('https://sandbox-checkout.paddle.com/api/1.0/order') @@ -66,19 +104,49 @@ def test_sandbox(paddle_client): error.match(msg) -def test_paddle_json_and_data(paddle_client): +def test_json_and_data(paddle_client): # NOQA: F811 with pytest.raises(ValueError) as error: - paddle_client.get('anyurl', json={'a': 'b'}, data={'a': 'b'}) + paddle_client.get( + paddle_client.default_url, + json={'a': 'b'}, + data={'a': 'b'} + ) error.match('Please set either data or json not both') -def test_paddle_data_and_json(paddle_client): +def test_bad_url(paddle_client): # NOQA: F811 with pytest.raises(PaddleException) as error: - paddle_client.get('/badurl') + with pytest.warns(RuntimeWarning) as warning: + paddle_client.get('/badurl') error.match('Paddle error 101 - Bad method call') - -def test_paddle_http_error(paddle_client): + warning_message = ( + 'Paddle recieved a relative URL so it will attempt to join it to ' + 'https://sandbox-vendors.paddle.com/api/2.0/ as it is the Paddle URL ' + 'with the most endpoints. The full URL that will be used is: ' + 'https://sandbox-vendors.paddle.com/api/2.0/badurl - You should ' + 'specifiy the full URL as this default URL may change in the future.' + ) + assert len(warning._list) == 1 + assert str(warning._list[0].message) == warning_message + + +def test_sandbox_warning(paddle_client): # NOQA: F811 + with pytest.warns(RuntimeWarning) as warning: + url = 'https://vendors.paddle.com/api/2.0/product/get_products' + paddle_client.post(url) + + sandbox_url = 'https://sandbox-vendors.paddle.com/api/2.0/product/get_products' # NOQA: E501 + warning_message = ( + 'PaddleClient is configured in sandbox mode but the URL provided does ' + 'not point to a sandbox subdomain. The URL will be converted to use ' + 'the Paddle sandbox ({0})'.format(sandbox_url) + ) + assert len(warning._list) == 1 + assert str(warning._list[0].message) == warning_message + + +def test_http_error(paddle_client): # NOQA: F811 with pytest.raises(PaddleException) as error: paddle_client.post('https://sandbox-checkout.paddle.com/api/1.0/order') From f495058b9a5ae5af5a2ea138ebe5e32ff01eebc2 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:36:47 +0000 Subject: [PATCH 17/30] Remove mocks and use fixtures instead depending on manual setup with env vars --- tests/test_coupons.py | 45 +--- tests/test_modifiers.py | 36 +--- tests/test_one_off_charges.py | 67 ++---- tests/test_order_information.py | 11 +- tests/test_plans.py | 70 +----- tests/test_prices.py | 35 ++- tests/test_product_payments.py | 68 ++---- tests/test_products.py | 5 +- tests/test_subscription_payments.py | 82 +++----- tests/test_subscription_users.py | 316 +++++++--------------------- tests/test_transactions.py | 31 ++- tests/test_user_history.py | 31 ++- tests/test_webhooks.py | 6 +- tox.ini | 3 +- 14 files changed, 195 insertions(+), 611 deletions(-) diff --git a/tests/test_coupons.py b/tests/test_coupons.py index efb05f8..46be566 100644 --- a/tests/test_coupons.py +++ b/tests/test_coupons.py @@ -1,43 +1,10 @@ -import os from datetime import datetime, timedelta import pytest from paddle import PaddleException -from .test_paddle import paddle_client # NOQA: F401 - - -@pytest.fixture() -def create_coupon(paddle_client): # NOQA: F811 - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - currency = 'GBP' - now = datetime.now().isoformat() - response = paddle_client.create_coupon( - coupon_type='product', - discount_type='percentage', - discount_amount=1, - allowed_uses=1, - recurring=False, - currency=currency, - product_ids=[product_id], - coupon_code='paddle-python-create_coupon_fixture-{0}'.format(now), - description='Test coupon created by paddle-python create_coupon_fixture', # NOQA: E501 - expires=datetime.today(), - minimum_threshold=9999, - group='paddle-python', - ) - coupon_code = response['coupon_codes'][0] - yield coupon_code, product_id - - try: - paddle_client.delete_coupon( - coupon_code=coupon_code, product_id=product_id - ) - except PaddleException as error: - valid_error = 'Paddle error 135 - Unable to find requested coupon' - if str(error) != valid_error: - raise +from .fixtures import create_coupon, get_product, paddle_client # NOQA: F401 def test_list_coupons(paddle_client, create_coupon): # NOQA: F811 @@ -57,22 +24,22 @@ def test_list_coupons(paddle_client, create_coupon): # NOQA: F811 def test_list_coupons_invalid_product(paddle_client): # NOQA: F811 - product_id = 11 + product_id = 9999999999 with pytest.raises(PaddleException) as error: paddle_client.list_coupons(product_id=product_id) error.match('Paddle error 108 - Unable to find requested product') -def test_create_coupon(paddle_client): # NOQA: F811 +def test_create_coupon(paddle_client, get_product): # NOQA: F811 coupon_type = 'product' discount_type = 'percentage' discount_amount = 1 allowed_uses = 1 recurring = False - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) + product_id = get_product['id'] product_ids = [product_id] - currency = 'GBP' + currency = 'USD' now = datetime.now().isoformat() coupon_code = 'paddle-python-test_create_coupon-{0}'.format(now) description = 'Test code created by paddle-python test_create_coupon' @@ -132,7 +99,7 @@ def test_update_coupon(paddle_client, create_coupon): # NOQA: F811 now = datetime.now().isoformat() new_coupon_code = 'paddle-python-test_update_coupon-{0}'.format(now) expires = (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d') - currency = 'GBP' + currency = 'USD' recurring = True allowed_uses = 2 discount_amount = 2 diff --git a/tests/test_modifiers.py b/tests/test_modifiers.py index 341467b..6139dde 100644 --- a/tests/test_modifiers.py +++ b/tests/test_modifiers.py @@ -1,36 +1,10 @@ -import os +from .fixtures import ( # NOQA: F401, E501 + create_modifier, create_plan, get_subscription, paddle_client +) -import pytest -from paddle import PaddleException - -from .test_paddle import paddle_client # NOQA: F401 - - -@pytest.fixture() -def create_modifier(paddle_client): # NOQA: F811 - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) - response = paddle_client.add_modifier( - subscription_id=subscription_id, - modifier_amount=0.01, - modifier_recurring=True, - modifier_description='test_modifier_fixture_modifier_description', - ) - modifier_id = response['modifier_id'] - subscription_id = response['subscription_id'] - - yield modifier_id, subscription_id - - try: - paddle_client.delete_modifier(modifier_id=modifier_id) - except PaddleException as error: - valid_error = 'Paddle error 123 - Unable to find requested modifier' - if str(error) != valid_error: - raise - - -def test_add_modifier(paddle_client): # NOQA: F811 - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) +def test_add_modifier(paddle_client, get_subscription): # NOQA: F811 + subscription_id = get_subscription['subscription_id'] response = paddle_client.add_modifier( subscription_id=subscription_id, modifier_amount=0.01, diff --git a/tests/test_one_off_charges.py b/tests/test_one_off_charges.py index ee8c85a..037583d 100644 --- a/tests/test_one_off_charges.py +++ b/tests/test_one_off_charges.py @@ -1,61 +1,17 @@ -import os from datetime import datetime import pytest from paddle import PaddleException -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import ( # NOQA: F401 + create_plan, get_subscription, paddle_client +) -@pytest.mark.mocked -def test_create_one_off_charge(mocker, paddle_client): # NOQA: F811 - """ - This test is mocked as creating a one off charge against Paddle will - actually create a one of charge - - If this test fails it means a change has been made which has affected - the one-off charge endpoint. - - The code now needs to be run directly against Paddle's API at least once to - ensure the new code is working as expected. - - Please uncomment the '@pytest.mark.skip()' line for the - 'test_create_one_off_charge_no_mock' test to run the create_one_off_charge - code against the Paddle API to check the changes work. - - Once the `test_create_one_off_charge_no_mock` test passes please update - the mock below and comment out the function again. - """ - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) - amount = 0.01 - charge_name = 'test_create_one_off_charge' - data = { - 'amount': amount, - 'charge_name': charge_name, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/subscription/{0}/charge'.format( - subscription_id - ) - url = paddle_client.get_environment_url(url) - method = 'POST' - request = mocker.patch('paddle.paddle.requests.request') - paddle_client.create_one_off_charge( - subscription_id=subscription_id, - amount=amount, - charge_name=charge_name - ) - request.assert_called_once_with(url=url, data=data, method=method) - - -# Comment out '@pytest.mark.skip()' to ensure the create_one_off_charge code -# is working as expected -@pytest.mark.skip() -def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811 - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) - amount = 1.00 +def test_create_one_off_charge(paddle_client, get_subscription): # NOQA: F811,E501 + subscription_id = get_subscription['subscription_id'] + amount = 5.00 response = paddle_client.create_one_off_charge( subscription_id=subscription_id, amount=amount, @@ -65,13 +21,18 @@ def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811 assert isinstance(response['currency'], str) assert isinstance(response['receipt_url'], str) assert response['subscription_id'] == subscription_id - assert response['amount'] == '%.2f' % round(amount, 2) + # There is a bug with the sandbox API where the ammount is returned + # with 3 decimal places. + if len(str(response['amount'])) == 5: + assert response['amount'] == '%.3f' % round(amount, 3) + else: + assert response['amount'] == '%.2f' % round(amount, 2) assert isinstance(response['payment_date'], str) datetime.strptime(response['payment_date'], '%Y-%m-%d') -def test_create_one_off_charge_zero_ammount(paddle_client): # NOQA: F811 - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) +def test_create_one_off_charge_zero_ammount(paddle_client, get_subscription): # NOQA: F811,E501 + subscription_id = get_subscription['subscription_id'] with pytest.raises(PaddleException) as error: paddle_client.create_one_off_charge( diff --git a/tests/test_order_information.py b/tests/test_order_information.py index e8db65a..028d11b 100644 --- a/tests/test_order_information.py +++ b/tests/test_order_information.py @@ -1,15 +1,14 @@ -import os - import pytest from paddle import PaddleException -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import ( # NOQA: F401 + create_plan, get_checkout, get_subscription, paddle_client +) -def test_get_order_details(paddle_client): # NOQA: F811 - # ToDo: Create / get a list of orders instead of hardcoding ID here - checkout_id = os.environ['PADDLE_TEST_DEFAULT_CHECKOUT_ID'] +def test_get_order_details(paddle_client, get_checkout): # NOQA: F811 + checkout_id = get_checkout response = paddle_client.get_order_details(checkout_id=checkout_id) assert len(response.keys()) == 4 assert response['checkout']['checkout_id'] == checkout_id diff --git a/tests/test_plans.py b/tests/test_plans.py index eb7b24a..6b2a057 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -1,12 +1,11 @@ -import os from datetime import datetime import pytest -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import create_plan, paddle_client # NOQA: F401 -def test_list_plans(paddle_client): # NOQA: F811 +def test_list_plans(paddle_client, create_plan): # NOQA: F811 # ToDo: Create plan when API exists for it here plan_list = paddle_client.list_plans() for plan in plan_list: @@ -19,9 +18,8 @@ def test_list_plans(paddle_client): # NOQA: F811 assert isinstance(plan['trial_days'], int) -def test_list_plans_with_plan(paddle_client): # NOQA: F811 - # ToDo: Create plan when API exists for it here - plan_id = int(os.environ['PADDLE_TEST_DEFAULT_PLAN_ID']) +def test_list_plans_with_plan_kwarg(paddle_client, create_plan): # NOQA: F811 + plan_id = create_plan['id'] plan_list = paddle_client.list_plans(plan=plan_id) assert len(plan_list) == 1 plan = plan_list[0] @@ -34,9 +32,8 @@ def test_list_plans_with_plan(paddle_client): # NOQA: F811 assert isinstance(plan['trial_days'], int) -def test_get_plan(paddle_client): # NOQA: F811 - # ToDo: Create plan when API exists for it here - plan_id = int(os.environ['PADDLE_TEST_DEFAULT_PLAN_ID']) +def test_get_plan(paddle_client, create_plan): # NOQA: F811 + plan_id = create_plan['id'] plan = paddle_client.get_plan(plan=plan_id) assert plan['id'] == plan_id assert isinstance(plan['billing_period'], int) @@ -96,61 +93,6 @@ def test_create_plan(paddle_client): # NOQA: F811 } -def test_create_plan_mock(mocker, paddle_client): # NOQA: F811 - request = mocker.patch('paddle.paddle.requests.request') - - now = datetime.now().isoformat() - plan_name = 'paddle-python-test_create_plan {0}'.format(now) - plan_trial_days = 999 - plan_length = 999 - plan_type = 'year' - main_currency_code = 'USD' - initial_price_usd = 0.0 - initial_price_gbp = 0.0 - initial_price_eur = 0.0 - recurring_price_usd = 0.0 - recurring_price_gbp = 0.0 - recurring_price_eur = 0.0 - - json = { - 'plan_name': plan_name, - 'plan_trial_days': plan_trial_days, - 'plan_length': plan_length, - 'plan_type': plan_type, - 'main_currency_code': main_currency_code, - 'initial_price_usd': initial_price_usd, - 'initial_price_gbp': initial_price_gbp, - 'initial_price_eur': initial_price_eur, - 'recurring_price_usd': recurring_price_usd, - 'recurring_price_gbp': recurring_price_gbp, - 'recurring_price_eur': recurring_price_eur, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/subscription/plans_create' - url = paddle_client.get_environment_url(url) - method = 'POST' - - paddle_client.create_plan( - plan_name=plan_name, - plan_trial_days=plan_trial_days, - plan_length=plan_length, - plan_type=plan_type, - main_currency_code=main_currency_code, - initial_price_usd=initial_price_usd, - initial_price_gbp=initial_price_gbp, - initial_price_eur=initial_price_eur, - recurring_price_usd=recurring_price_usd, - recurring_price_gbp=recurring_price_gbp, - recurring_price_eur=recurring_price_eur, - ) - request.assert_called_once_with( - url=url, - json=json, - method=method, - ) - - @pytest.mark.parametrize( 'currency,missing_field', [ diff --git a/tests/test_prices.py b/tests/test_prices.py index 6492c43..911c097 100644 --- a/tests/test_prices.py +++ b/tests/test_prices.py @@ -1,13 +1,10 @@ -import os - import pytest -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import get_product, paddle_client # NOQA: F401 -def test_get_prices(paddle_client): # NOQA: F811 - # ToDo: get list of orders here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) +def test_get_prices(paddle_client, get_product): # NOQA: F811 + product_id = get_product['id'] response = paddle_client.get_prices(product_ids=[product_id]) assert len(response.keys()) == 2 assert 'customer_country' in response @@ -20,10 +17,9 @@ def test_get_prices(paddle_client): # NOQA: F811 assert 'vendor_set_prices_included_tax' in product -def test_get_prices_with_customer_country(paddle_client): # NOQA: F811 - # ToDo: Get list of orders here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - country = 'GB' +def test_get_prices_with_customer_country(paddle_client, get_product): # NOQA: F811,E501 + product_id = get_product['id'] + country = 'US' response = paddle_client.get_prices( product_ids=[product_id], customer_country=country ) @@ -33,9 +29,8 @@ def test_get_prices_with_customer_country(paddle_client): # NOQA: F811 assert 'product_id' in product -def test_get_prices_invalid_customer_country(paddle_client): # NOQA: F811 - # ToDo: Get list of orders here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) +def test_get_prices_invalid_customer_country(paddle_client, get_product): # NOQA: F811,E501 + product_id = get_product['id'] bad_country = '00' value_error = 'Country code "{0}" is not valid'.format(bad_country) @@ -47,22 +42,20 @@ def test_get_prices_invalid_customer_country(paddle_client): # NOQA: F811 error.match(value_error) -def test_get_prices_with_customer_ip(paddle_client): # NOQA: F811 - # ToDo: Get list of orders here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - ip = '109.144.232.226' # https://tools.tracemyip.org/search--city/london +def test_get_prices_with_customer_ip(paddle_client, get_product): # NOQA: F811 + product_id = get_product['id'] + ip = '8.47.69.211' # https://tools.tracemyip.org/search--city/los+angeles # NOQA: E501 response = paddle_client.get_prices( product_ids=[product_id], customer_ip=ip ) assert len(response.keys()) == 2 - assert response['customer_country'] == 'GB' + assert response['customer_country'] == 'US' for product in response['products']: assert product['product_id'] == product_id -def test_get_prices_with_coupons(paddle_client): # NOQA: F811 - # ToDo: get list of orders here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) +def test_get_prices_with_coupons(paddle_client, get_product): # NOQA: F811 + product_id = get_product['id'] coupon_code = 'COUPONOFF' response = paddle_client.get_prices( product_ids=[product_id], coupons=[coupon_code] diff --git a/tests/test_product_payments.py b/tests/test_product_payments.py index 38f4ab1..938e4f4 100644 --- a/tests/test_product_payments.py +++ b/tests/test_product_payments.py @@ -1,66 +1,22 @@ -import os +from .fixtures import ( # NOQA: F401 + create_plan, get_subscription, paddle_client +) -import pytest -from .test_paddle import paddle_client # NOQA: F401 - - -@pytest.mark.mocked -def test_refund_product_payment(mocker, paddle_client): # NOQA: F811 - """ - This test is mocked as creating a refund is not something you want to - happen against a live system. - - If this test fails it means a change has been made which has affected - the refund product payment endpoint. - - The code now needs to be run directly against Paddle's API at least once to - ensure the new code is working as expected. - - Please uncomment the '@pytest.mark.skip()' line for the - 'test_refund_product_payment_no_mock' test to run the the - refund_product_payment code against the Paddle API to check the changes - work. - - Once the `test_refund_product_payment_no_mock` test passes please update - the mock below and comment out the function again. - """ - order_id = 123 - amount = 0.1 - reason = 'paddle-python-test_refund_product_payment' - json = { - 'order_id': order_id, - 'amount': amount, - 'reason': reason, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/payment/refund' - url = paddle_client.get_environment_url(url) - method = 'POST' - request = mocker.patch('paddle.paddle.requests.request') - paddle_client.refund_product_payment( - order_id=order_id, - amount=amount, - reason=reason - ) - request.assert_called_once_with(url=url, json=json, method=method) - - -# Comment out '@pytest.mark.skip()' to ensure the refund_product_payment code -# is working as expected -@pytest.mark.skip() -def test_refund_product_payment_no_mock(paddle_client): # NOQA: F811 +def test_refund_product_payment(paddle_client, get_subscription): # NOQA: F811,E501 """ If you get the error: "Paddle error 172 - The transaction can no longer be refunded."" - You will need to manually enter a subscription_id below. - (this is why it's mocked in the first place, it's a pain sorry) + You will need to create a new payment. See Creating a subscription in + CONTRIButiNG.md for instructions """ - order_id = 1 # This will need to be manually entered + subscription_list = paddle_client.list_transactions( + entity='subscription', + entity_id=get_subscription['subscription_id'], + ) response = paddle_client.refund_product_payment( - order_id=order_id, - amount=0.1, + order_id=subscription_list[0]['order_id'], + amount=0.01, reason='paddle-python-test_refund_product_payment' ) assert isinstance(response['refund_request_id'], int) diff --git a/tests/test_products.py b/tests/test_products.py index a93de58..12f5246 100644 --- a/tests/test_products.py +++ b/tests/test_products.py @@ -1,8 +1,7 @@ -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import get_product, paddle_client # NOQA: F401 -def test_list_products(paddle_client): # NOQA: F811 - # ToDo: Create product when API exists for it here +def test_list_products(paddle_client, get_product): # NOQA: F811 response = paddle_client.list_products() assert 'count' in response assert 'total' in response diff --git a/tests/test_subscription_payments.py b/tests/test_subscription_payments.py index c41cb1f..56830b7 100644 --- a/tests/test_subscription_payments.py +++ b/tests/test_subscription_payments.py @@ -1,28 +1,18 @@ -import os import warnings from datetime import datetime, timedelta -import pytest +from .fixtures import ( # NOQA: F401 + create_plan, get_subscription, paddle_client +) +from .test_paddle import BadPaddleDataWarning -from paddle import PaddleException -from .test_paddle import BadPaddleDataWarning, paddle_client # NOQA: F401 - - -def test_list_subscription_payments(paddle_client): # NOQA: F811 +def test_list_subscription_payments(paddle_client, get_subscription): # NOQA: F811,E501 response = paddle_client.list_subscription_payments() - - if not response: - warning = ('No subscription payments returned by ' - 'list_subscription_payments in test_list_subscription_payments') # NOQA: E501 - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) - for payment in response: assert isinstance(payment['id'], int) assert isinstance(payment['subscription_id'], int) - assert isinstance(payment['amount'], int) + assert type(payment['amount']) in [int, float] assert isinstance(payment['currency'], str) assert isinstance(payment['payout_date'], str) datetime.strptime(payment['payout_date'], '%Y-%m-%d') @@ -32,44 +22,26 @@ def test_list_subscription_payments(paddle_client): # NOQA: F811 assert isinstance(payment['receipt_url'], str) -def test_list_subscription_payments_with_subscription_id(paddle_client): # NOQA: F811,E501 - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) +def test_list_subscription_payments_with_subscription_id(paddle_client, get_subscription): # NOQA: F811,E501 + subscription_id = get_subscription['subscription_id'] response = paddle_client.list_subscription_payments( subscription_id=subscription_id ) - - if not response: - warning = ('No subscription payments returned by ' - 'list_subscription_payments in test_list_subscription_payments_with_subscription_id') # NOQA: E501 - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) - for payment in response: assert payment['subscription_id'] == subscription_id -def test_list_subscription_payments_with_plan_id(paddle_client): # NOQA: F811 - plan_id = int(os.environ['PADDLE_TEST_DEFAULT_PLAN_ID']) +def test_list_subscription_payments_with_plan_id(paddle_client, create_plan): # NOQA: F811,E501 + plan_id = create_plan['id'] response = paddle_client.list_subscription_payments(plan=plan_id) - - if not response: - warning = ('No subscription payments returned by ' - 'list_subscription_payments in test_list_subscription_payments_with_plan_id') # NOQA: E501 - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) + for payment in response: + assert 'id' in payment + assert 'amount' in payment + assert 'currency' in payment -def test_list_subscription_payments_with_from_to(paddle_client): # NOQA: F811 +def test_list_subscription_payments_with_from_to(paddle_client, get_subscription): # NOQA: F811,E501 all_payments = paddle_client.list_subscription_payments() - if not all_payments: - warning = ('No subscription payments returned by ' - 'list_subscription_payments in test_list_subscription_payments_with_from_to') # NOQA: E501 - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) - single_payment = all_payments[0] single_payout_date = datetime.strptime(single_payment['payout_date'], '%Y-%m-%d') # NOQA: E501 @@ -85,35 +57,33 @@ def test_list_subscription_payments_with_from_to(paddle_client): # NOQA: F811 assert payout_date < to -def test_list_subscription_payments_with_is_paid(paddle_client): # NOQA: F811,E501 - response = paddle_client.list_subscription_payments( - is_paid=False - ) - +def test_list_subscription_payments_with_is_paid(paddle_client, get_subscription): # NOQA: F811,E501 + response = paddle_client.list_subscription_payments(is_paid=False) if not response: + subscription_payments = paddle_client.list_subscription_payments() + assert len(subscription_payments) != len(response) + warning = ('No subscription payments returned by ' 'list_subscription_payments in test_list_subscription_payments_with_is_paid') # NOQA: E501 warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) for payment in response: assert payment['is_paid'] == 0 -# ToDo: Fix this -@pytest.mark.xfail(raises=PaddleException, reason='Date issue, unsure why') def test_reschedule_subscription_payment(paddle_client): # NOQA: F811 all_payments = paddle_client.list_subscription_payments( is_paid=False, is_one_off_charge=False, ) if not all_payments: - warning = ('No subscription payments returned by ' - 'list_subscription_payments in test_refund_product_payments') # NOQA + subscription_payments = paddle_client.list_subscription_payments() + assert len(subscription_payments) != len(all_payments) + warning = ( + 'No subscription payments returned by list_subscription_payments ' + 'in test_refund_product_payments' + ) warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501 - pytest.skip(skip_message) payment = all_payments[0] single_payout_date = datetime.strptime(payment['payout_date'], '%Y-%m-%d') diff --git a/tests/test_subscription_users.py b/tests/test_subscription_users.py index f592c47..bd9e457 100644 --- a/tests/test_subscription_users.py +++ b/tests/test_subscription_users.py @@ -1,13 +1,12 @@ -import os -import warnings from datetime import datetime import pytest +from .fixtures import create_plan, get_subscription # NOQA: F401 from .test_paddle import BadPaddleDataWarning, paddle_client # NOQA: F401 -def test_list_subscription_users(paddle_client): # NOQA: F811 +def test_list_subscription_users(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here subscription_users = paddle_client.list_subscription_users() for subscription in subscription_users: @@ -27,220 +26,77 @@ def test_list_subscription_users(paddle_client): # NOQA: F811 assert isinstance(subscription['linked_subscriptions'], list) -def test_list_subscription_users_with_subscription_id(paddle_client): # NOQA: F811,E501 +def test_list_subscription_users_with_subscription_id(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here - response = paddle_client.list_subscription_users(results_per_page=1) - try: - first_subscription = response[0] - except IndexError: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_subscription_id') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) - - subscription_id = first_subscription['subscription_id'] + subscription_id = get_subscription['subscription_id'] subscription_users = paddle_client.list_subscription_users( - subscription_id=first_subscription['subscription_id'], + subscription_id=subscription_id, ) for subscription in subscription_users: assert subscription['subscription_id'] == subscription_id -def test_list_subscription_users_with_plan_id(paddle_client): # NOQA: F811 +def test_list_subscription_users_with_plan_id(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here - response = paddle_client.list_subscription_users(results_per_page=1) - try: - first_subscription = response[0] - except IndexError: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_plan_id') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) - subscription_users = paddle_client.list_subscription_users( - plan_id=first_subscription['plan_id'], - ) + + plan_id = get_subscription['plan_id'] + subscription_users = paddle_client.list_subscription_users(plan_id=plan_id) for subscription in subscription_users: - assert subscription['plan_id'] == first_subscription['plan_id'] + assert subscription['plan_id'] == plan_id -def test_list_subscription_users_with_state(paddle_client): # NOQA: F811 +def test_list_subscription_users_with_state(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here - response = paddle_client.list_subscription_users(results_per_page=1) - try: - first_subscription = response[0] - except IndexError: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_state') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) - subscription_users = paddle_client.list_subscription_users( - state=first_subscription['state'], - ) + state = get_subscription['state'] + subscription_users = paddle_client.list_subscription_users(state=state) for subscription in subscription_users: - assert subscription['state'] == first_subscription['state'] + assert subscription['state'] == state -def test_list_subscription_users_with_page(paddle_client): # NOQA: F811 +def test_list_subscription_users_with_page(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here list_one = paddle_client.list_subscription_users( results_per_page=1, page=1, ) - if not list_one: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_page') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) list_two = paddle_client.list_subscription_users( results_per_page=1, page=2, ) assert list_one != list_two -def test_list_subscription_users_with_results_per_page(paddle_client): # NOQA: F811,E501 +def test_list_subscription_users_with_results_per_page(paddle_client, get_subscription): # NOQA: F811,E501 # ToDo: Create plan when API exists for it here list_one = paddle_client.list_subscription_users( results_per_page=1, page=1, ) - if not list_one: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_page') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) assert len(list_one) == 1 -@pytest.mark.mocked -def test_cancel_subscription(mocker, paddle_client): # NOQA: F811 - """ - This test is mocked as canceling a subscription is not something you want - to do against a live system. - - If this test fails it means a change has been made which has affected - the cancel subscription endpoint. - - The code now needs to be run directly against Paddle's API at least once to - ensure the new code is working as expected. - - Please uncomment the '@pytest.mark.skip()' line for the - 'cancel_subscription_no_mock' test to run the the cancel_subscription code - against the Paddle API to check the changes work. - - Once the `cancel_subscription_no_mock` test passes please update - the mock below and comment out the function again. - """ - subscription_id = 123 - json = { - 'subscription_id': subscription_id, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/subscription/users_cancel' - url = paddle_client.get_environment_url(url) - method = 'POST' - request = mocker.patch('paddle.paddle.requests.request') - paddle_client.cancel_subscription( - subscription_id=subscription_id, - ) - request.assert_called_once_with(url=url, json=json, method=method) - - -# Comment out '@pytest.mark.skip()' to ensure the cancel_subscription -# code is working as expected -@pytest.mark.skip() -def test_cancel_subscription_no_mock(paddle_client): # NOQA: F811 - """ - If you get the error: - "Paddle error 119 - Unable to find requested subscription"" - You will need to manually enter a subscription_id below. - (this is why it's mocked in the first place, it's a pain sorry) - """ - subscription_id = 1 # This will need to be manually entered - response = paddle_client.cancel_subscription( +def test_update_subscription(paddle_client, get_subscription): # NOQA: F811 + subscription_id = get_subscription['subscription_id'] + new_quantity = get_subscription['quantity'] + 0.01 + paddle_client.update_subscription( subscription_id=subscription_id, + quantity=new_quantity, ) - assert response is True - - -@pytest.mark.mocked -def test_update_subscription(mocker, paddle_client): # NOQA: F811 - """ - This test is mocked as updating a subscription is probably not something - you want to do on a live system. - - If this test fails it means a change has been made which has affected - the update subscription endpoint. - - The code now needs to be run directly against Paddle's API at least once to - ensure the new code is working as expected. - - Please uncomment the '@pytest.mark.skip()' line for the - 'update_subscription_no_mock' test to run the the update_subscription code - against the Paddle API to check the changes work. - - Once the `update_subscription_no_mock` test passes please update - the mock below and comment out the function again. - """ - subscription_id = 123 - quantity = 0.1 - currency = 'GBP' - recurring_price = 0.1 - bill_immediately = False - plan_id = int(os.environ['PADDLE_TEST_DEFAULT_PLAN_ID']) - prorate = False - keep_modifiers = True - passthrough = 'passthrough-update-test_update_subscription' - json = { - 'subscription_id': subscription_id, - 'quantity': quantity, - 'currency': currency, - 'recurring_price': recurring_price, - 'bill_immediately': bill_immediately, - 'plan_id': plan_id, - 'prorate': prorate, - 'keep_modifiers': keep_modifiers, - 'passthrough': passthrough, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' - url = paddle_client.get_environment_url(url) - method = 'POST' - request = mocker.patch('paddle.paddle.requests.request') - paddle_client.update_subscription( + new_subscription_data = paddle_client.list_subscription_users( subscription_id=subscription_id, - quantity=quantity, - currency=currency, - recurring_price=recurring_price, - bill_immediately=bill_immediately, - plan_id=plan_id, - prorate=prorate, - keep_modifiers=keep_modifiers, - passthrough=passthrough, ) - request.assert_called_once_with(url=url, json=json, method=method) + assert new_subscription_data.quantity == new_quantity -# Comment out '@pytest.mark.skip()' to ensure the update_subscription -# code is working as expected -@pytest.mark.skip() -def test_update_subscription_no_mock(paddle_client): # NOQA: F811 +def test_update_subscription(paddle_client, get_subscription): # NOQA: F811 """ If you get the error: Unable to find subscription with id 1 You will need to manually enter a subscription_id below. (this is why it's mocked in the first place, it's a pain sorry) """ - subscription_id = 1 # This will need to be manually entered - subscription_data = get_subscription(paddle_client, subscription_id) + subscription_id = get_subscription['subscription_id'] # Can't udate passthrough (least destructive) as 'list_subscription_users' # does not return it in the response - started_at_paused = 'paused_at' in subscription_data + started_at_paused = 'paused_at' in get_subscription pause = not started_at_paused response = paddle_client.update_subscription( subscription_id=subscription_id, @@ -250,7 +106,11 @@ def test_update_subscription_no_mock(paddle_client): # NOQA: F811 assert isinstance(response['user_id'], int) assert isinstance(response['plan_id'], int) assert isinstance(response['next_payment'], dict) - new_subscription_data = get_subscription(paddle_client, subscription_id) + + new_subscription_data = paddle_client.list_subscription_users( + subscription_id=subscription_id, + ) + new_subscription_data = new_subscription_data[0] if started_at_paused: assert 'paused_at' not in new_subscription_data @@ -269,98 +129,64 @@ def test_update_subscription_no_mock(paddle_client): # NOQA: F811 pause=not pause, ) - -def get_subscription(paddle_client, subscription_id): # NOQA: F811 - subscription_users = paddle_client.list_subscription_users() - for subscription in subscription_users: - if subscription['subscription_id'] == subscription_id: - return subscription - raise ValueError('Unable to find subscription with id {0}'.format(subscription_id)) # NOQA: E501 + # Test the change back worked + new_subscription_data = paddle_client.list_subscription_users( + subscription_id=subscription_id, + ) + new_subscription_data = new_subscription_data[0] + if started_at_paused: + assert isinstance(new_subscription_data['paused_at'], str) + datetime.strptime(new_subscription_data['paused_at'], '%Y-%m-%d %H:%M:%S') # NOQA: E501 + assert isinstance(new_subscription_data['paused_from'], str) + datetime.strptime(new_subscription_data['paused_from'], '%Y-%m-%d %H:%M:%S') # NOQA: E501 + assert new_subscription_data['paused_reason'] == 'voluntary' + else: + assert 'paused_at' not in new_subscription_data + assert 'paused_from' not in new_subscription_data + assert 'paused_reason' not in new_subscription_data @pytest.mark.mocked -def test_pause_subscription(mocker, paddle_client): # NOQA: F811 +def test_cancel_subscription(mocker, paddle_client): # NOQA: F811 """ - This test is mocked as pausing a subscription is probably not something - you want to do on a live system. + This test is mocked as subscriptions must be created manually (see + `Creating a subscription` in CONTRIBUTING.md) as there is no API + to do so If this test fails it means a change has been made which has affected - the update subscription endpoint. Please see test_update_subscription - one what to do now. - """ - subscription_id = 123 - json = { - 'subscription_id': subscription_id, - 'pause': True, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' - url = paddle_client.get_environment_url(url) - method = 'POST' - request = mocker.patch('paddle.paddle.requests.request') - paddle_client.pause_subscription(subscription_id=subscription_id) - request.assert_called_once_with(url=url, json=json, method=method) + the cancel subscription endpoint. + The code now needs to be run directly against Paddle's API at least once to + ensure the new code is working as expected. -@pytest.mark.mocked -def test_resume_subscription(mocker, paddle_client): # NOQA: F811 - """ - This test is mocked as pausing a subscription is probably not something - you want to do on a live system. + Please uncomment the '@pytest.mark.skip()' line for the + 'cancel_subscription_no_mock' test to run the the cancel_subscription code + against the Paddle API to check the changes work. - If this test fails it means a change has been made which has affected - the update subscription endpoint. Please see test_update_subscription - one what to do now. + Once the `cancel_subscription_no_mock` test passes please update + the mock below and comment out the function again. """ subscription_id = 123 json = { 'subscription_id': subscription_id, - 'pause': False, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], + 'vendor_id': paddle_client.vendor_id, + 'vendor_auth_code': paddle_client.api_key, } - url = 'https://vendors.paddle.com/api/2.0/subscription/users/update' - url = paddle_client.get_environment_url(url) + url = 'https://sandbox-vendors.paddle.com/api/2.0/subscription/users_cancel' # NOQA: E501 method = 'POST' request = mocker.patch('paddle.paddle.requests.request') - paddle_client.resume_subscription(subscription_id=subscription_id) + paddle_client.cancel_subscription( + subscription_id=subscription_id, + ) request.assert_called_once_with(url=url, json=json, method=method) -def test_preview_subscription_update(mocker, paddle_client): # NOQA: F811 - subscription_data = {} - subscription_users = paddle_client.list_subscription_users() - for subscription in subscription_users: - if 'paused_at' not in subscription and subscription['state'] == 'active': # NOQA: E501 - subscription_data = subscription - if not subscription_data: - warning = ('No subscriptions returned by list_subscription_users() in ' - 'test_list_subscription_users_with_subscription_id') - warnings.warn(warning, BadPaddleDataWarning) - skip_message = ('list_subscription_users did not return any user subscription') # NOQA: E501 - pytest.skip(skip_message) - - subscription_id = subscription_data['subscription_id'] - quantity = subscription_data['quantity'] - amount = subscription_data['next_payment']['amount'] - currency = subscription_data['next_payment']['currency'] - new_quantity = quantity + 1 - expected_amount = new_quantity * amount - response = paddle_client.preview_update_subscription( +# Comment out '@pytest.mark.skip()' to ensure the cancel_subscription +# code is working as expected +@pytest.mark.skip() +def test_cancel_subscription_no_mock(paddle_client, get_subscription): # NOQA: F811,E501 + subscription_id = get_subscription + response = paddle_client.cancel_subscription( subscription_id=subscription_id, - bill_immediately=True, - quantity=new_quantity, ) - assert response['subscription_id'] == subscription_id - assert isinstance(response['plan_id'], int) - assert isinstance(response['user_id'], int) - assert type(response['immediate_payment']['amount']) in [int, float] - assert response['immediate_payment']['currency'] == currency - assert isinstance(response['immediate_payment']['date'], str) - datetime.strptime(response['immediate_payment']['date'], '%Y-%m-%d') - - assert response['next_payment']['amount'] == expected_amount - assert response['next_payment']['currency'] == currency - assert isinstance(response['next_payment']['date'], str) - datetime.strptime(response['next_payment']['date'], '%Y-%m-%d') + assert response is True diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 9c90402..b883390 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1,12 +1,12 @@ -import os from datetime import datetime -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import ( # NOQA: F401 + create_plan, get_checkout, get_product, get_subscription, paddle_client +) -def test_list_transactions_subscription(paddle_client): # NOQA: F811 - # ToDo: Create plan when API exists for it here - subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID']) +def test_list_transactions_subscription(paddle_client, get_subscription): # NOQA: F811,E501 + subscription_id = get_subscription['subscription_id'] subscription_list = paddle_client.list_transactions( entity='subscription', entity_id=subscription_id, @@ -19,8 +19,8 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 assert isinstance(plan['status'], str) assert isinstance(plan['created_at'], str) datetime.strptime(plan['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(plan['passthrough'], str) \ - or plan['passthrough'] is None + if plan['passthrough']: + assert isinstance(plan['passthrough'], str) assert isinstance(plan['product_id'], int) assert plan['is_subscription'] is True assert isinstance(plan['is_one_off'], bool) @@ -32,9 +32,8 @@ def test_list_transactions_subscription(paddle_client): # NOQA: F811 assert isinstance(plan['receipt_url'], str) -def test_list_transactions_product(paddle_client): # NOQA: F811 - # ToDo: Create product when API exists for it here - product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) +def test_list_transactions_product(paddle_client, get_product): # NOQA: F811 + product_id = get_product['id'] product_list = paddle_client.list_transactions( entity='product', entity_id=product_id, @@ -47,8 +46,8 @@ def test_list_transactions_product(paddle_client): # NOQA: F811 assert isinstance(product['status'], str) assert isinstance(product['created_at'], str) datetime.strptime(product['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(product['passthrough'], str) \ - or product['passthrough'] is None + if product['passthrough']: + assert isinstance(product['passthrough'], str) assert isinstance(product['product_id'], int) assert product['is_subscription'] is False assert isinstance(product['is_one_off'], bool) @@ -60,8 +59,8 @@ def test_list_transactions_product(paddle_client): # NOQA: F811 assert isinstance(product['receipt_url'], str) -def test_list_transactions_checkout(paddle_client): # NOQA: F811 - checkout_id = os.environ['PADDLE_TEST_DEFAULT_CHECKOUT_ID'] +def test_list_transactions_checkout(paddle_client, get_checkout): # NOQA: F811 + checkout_id = get_checkout checkout_list = paddle_client.list_transactions( entity='checkout', entity_id=checkout_id, @@ -75,8 +74,8 @@ def test_list_transactions_checkout(paddle_client): # NOQA: F811 assert isinstance(checkout['status'], str) assert isinstance(checkout['created_at'], str) datetime.strptime(checkout['created_at'], '%Y-%m-%d %H:%M:%S') - assert isinstance(checkout['passthrough'], str) \ - or checkout['passthrough'] is None + if checkout['passthrough']: + assert isinstance(checkout['passthrough'], str) assert isinstance(checkout['product_id'], int) assert isinstance(checkout['is_subscription'], bool) assert isinstance(checkout['is_one_off'], bool) diff --git a/tests/test_user_history.py b/tests/test_user_history.py index 37e9b73..0be8da3 100644 --- a/tests/test_user_history.py +++ b/tests/test_user_history.py @@ -1,26 +1,27 @@ import pytest -from paddle import PaddleClient, PaddleException +from paddle import PaddleException -from .test_paddle import paddle_client, unset_vendor_id # NOQA: F401 +from .fixtures import ( # NOQA: F401 + create_plan, get_subscription, paddle_client +) -def test_get_user_history_with_vendor_id(unset_vendor_id): # NOQA: F811 - email = 'test@example.com' - vendor_id = 11 # This will need to be manually entered - paddle = PaddleClient(vendor_id=vendor_id) - response = paddle.get_user_history(email=email, vendor_id=vendor_id) +def test_get_user_history_with_vendor_id(paddle_client, get_subscription): # NOQA: F811,E501 + email = get_subscription['user_email'] + vendor_id = paddle_client.vendor_id + response = paddle_client.get_user_history(email=email, vendor_id=vendor_id) assert response == 'We\'ve sent details of your past transactions, licenses and downloads to you via email.' # NOQA: E501 -def test_get_user_history_with_vendor_id_env_var(paddle_client): # NOQA: F811 - email = 'test@example.com' +def test_get_user_history_with_vendor_id_env_var(paddle_client, get_subscription): # NOQA: F811,E501 + email = get_subscription['user_email'] response = paddle_client.get_user_history(email=email) assert response == 'We\'ve sent details of your past transactions, licenses and downloads to you via email.' # NOQA: E501 -def test_get_user_history_with_product_id(paddle_client): # NOQA: F811 - email = 'test@example.com' +def test_get_user_history_with_product_id(paddle_client, get_subscription): # NOQA: F811,E501 + email = get_subscription['user_email'] product_id = 1 with pytest.raises(PaddleException) as error: paddle_client.get_user_history(email=email, product_id=product_id) @@ -29,9 +30,7 @@ def test_get_user_history_with_product_id(paddle_client): # NOQA: F811 error.match(msg) -def test_get_user_history_missing_vendoer_id_and_product_id(unset_vendor_id): # NOQA: F811, E501 - email = 'test@example.com' - vendor_id = 11 # This will need to be manually entered - paddle = PaddleClient(vendor_id=vendor_id) - response = paddle.get_user_history(email=email) +def test_get_user_history_missing_vendor_id_and_product_id(paddle_client, get_subscription): # NOQA: F811, E501 + email = get_subscription['user_email'] + response = paddle_client.get_user_history(email=email) assert response == 'We\'ve sent details of your past transactions, licenses and downloads to you via email.' # NOQA: E501 diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 22fd060..2e2c21b 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from .test_paddle import paddle_client # NOQA: F401 +from .fixtures import paddle_client # NOQA: F401 def test_get_webhook_history(paddle_client): # NOQA: F811 @@ -40,8 +40,8 @@ def test_get_webhook_history_head_and_tail(paddle_client): # NOQA: F811 webhook = base_webhook_history['data'][0] head = datetime.strptime(webhook['created_at'], '%Y-%m-%d %H:%M:%S') - new_head = head + timedelta(minutes=30) - new_tail = head - timedelta(minutes=30) + new_head = head + timedelta(minutes=5) + new_tail = head - timedelta(minutes=5) webhook_history = paddle_client.get_webhook_history( query_head=new_head.strftime('%Y-%m-%d %H:%M:%S'), query_tail=new_tail, diff --git a/tox.ini b/tox.ini index 851c1af..7233b0f 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ envlist = [testenv] -passenv = PADDLE_* deps = pytest pytest-cov @@ -16,7 +15,7 @@ deps = coverage commands = - pytest -m "not manual_cleanup" tests/ + pytest tests/ [testenv:lint] From 1bfc8a225a24e0414d70963445642213b1fc3edb Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:37:26 +0000 Subject: [PATCH 18/30] Test create_pay_link exceptions --- tests/test_pay_links.py | 227 +++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 105 deletions(-) diff --git a/tests/test_pay_links.py b/tests/test_pay_links.py index abb513d..8fe7c09 100644 --- a/tests/test_pay_links.py +++ b/tests/test_pay_links.py @@ -1,124 +1,141 @@ -import os - import pytest -from .test_paddle import paddle_client # NOQA: F401 - +from .fixtures import get_product, paddle_client # NOQA: F401 -@pytest.mark.manual_cleanup -def test_create_pay_link(paddle_client): # NOQA: F811 - create_pay_link = getattr(paddle_client, 'create_pay_link', None) - if not create_pay_link or not callable(create_pay_link): - pytest.skip('paddle.create_pay_link does not exist') - # ToDo: Create product when API exists for it here +def test_create_pay_link(paddle_client, get_product): # NOQA: F811 response = paddle_client.create_pay_link( - # product_id=int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']), + product_id=get_product['id'], title='paddle-python-test_create_pay_link', - webhook_url='https://example.com/paddle-python', prices=['USD:19.99'], - # recurring_prices=['USD:19.99'], + recurring_prices=['USD:19.99'], trial_days=1, custom_message='custom_message', - coupon_code='paddle-python-coupon_code', discountable=False, image_url='https://example.com/image_url', return_url='https://example.com/return_url', quantity_variable=1, quantity=1, - affiliates=['12345:0.25'], recurring_affiliate_limit=1, - # marketing_consent='0', + marketing_consent=True, customer_email='test@example.com', - customer_country='GB', - customer_postcode='SW1A 1AA', + customer_country='US', + customer_postcode='00000', passthrough='passthrough data', + vat_number="vat_number", + vat_company_name="vat_company_name", + vat_street="vat_street", + vat_city="vat_city", + vat_state="vat_state", + vat_country="vat_country", + vat_postcode="vat_postcode", + # affiliates=['12345:0.25'], + # coupon_code='paddle-python-coupon_code', + # webhook_url='https://example.com/paddle-python', ) - assert 'url' in response - - -def test_create_pay_link_mock(mocker, paddle_client): # NOQA: F811 - """ - Mock test as the above test is not run by tox due to manual_cleanup mark - """ - create_pay_link = getattr(paddle_client, 'create_pay_link', None) - if not create_pay_link or not callable(create_pay_link): - pytest.skip('paddle.create_pay_link does not exist') - - request = mocker.patch('paddle.paddle.requests.request') - - # product_id = int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']) - title = 'paddle-python-test_create_pay_link' - webhook_url = 'https://example.com/paddle-python' - prices = ['USD:19.99'] - trial_days = 1 - custom_message = 'custom_message' - coupon_code = 'paddle-python-coupon_code' - discountable = False - image_url = 'https://example.com/image_url' - return_url = 'https://example.com/return_url' - quantity_variable = 0 - quantity = 1 - affiliates = ['12345:0.25'] - recurring_affiliate_limit = 1 - # marketing_consent = False - customer_email = 'test@example.com' - customer_country = 'GB' - customer_postcode = 'SW1A 1AA' - passthrough = 'passthrough data' - - json = { - # 'product_id': product_id, - 'title': title, - 'webhook_url': webhook_url, - 'prices': prices, - # recurring_prices=prices, - 'trial_days': trial_days, - 'custom_message': custom_message, - 'coupon_code': coupon_code, - 'discountable': 1 if discountable else 0, - 'image_url': image_url, - 'return_url': return_url, - 'quantity_variable': quantity_variable, - 'quantity': quantity, - 'affiliates': affiliates, - 'recurring_affiliate_limit': recurring_affiliate_limit, - # 'marketing_consent': '1' if marketing_consent else '0', - 'customer_email': customer_email, - 'customer_country': customer_country, - 'customer_postcode': customer_postcode, - 'passthrough': passthrough, - 'vendor_id': int(os.environ['PADDLE_VENDOR_ID']), - 'vendor_auth_code': os.environ['PADDLE_API_KEY'], - } - url = 'https://vendors.paddle.com/api/2.0/product/generate_license' - url = paddle_client.get_environment_url(url) - method = 'POST' - - paddle_client.create_pay_link( - # product_id=int(os.environ['PADDLE_TEST_DEFAULT_PRODUCT_ID']), - title=title, - webhook_url=webhook_url, - prices=prices, - # recurring_prices=prices, - trial_days=trial_days, - custom_message=custom_message, - coupon_code=coupon_code, - discountable=discountable, - image_url=image_url, - return_url=return_url, - quantity_variable=quantity_variable, - quantity=quantity, - affiliates=affiliates, - recurring_affiliate_limit=recurring_affiliate_limit, - # marketing_consent=marketing_consent, - customer_email=customer_email, - customer_country=customer_country, - customer_postcode=customer_postcode, - passthrough=passthrough, - ) - request.assert_called_once_with( - url=url, - json=json, - method=method, + assert isinstance(response['url'], str) + assert response['url'].startswith('https://sandbox-checkout.paddle.com/checkout/custom/') # NOQA: E501 + + +def test_create_pay_link_no_product_or_title(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link() + error.match('title must be set if product_id is not set') + + +def test_create_pay_link_no_product_or_webhook(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link(title='test') + error.match('webhook_url must be set if product_id is not set') + + +def test_create_pay_link_no_product_reccuring(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', + recurring_prices=['USD:19.99'] + ) + error.match('webhook_url must be set if product_id is not set') + + +def test_create_pay_link_product_and_webhook(paddle_client, get_product): # NOQA: F811,E501 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + product_id=get_product['id'], + webhook_url='https://example.com/paddle-python', + ) + error.match('product_id and webhook_url cannot both be set') + + +def test_create_pay_link_invalid_country(paddle_client): # NOQA: F811 + country = 'FAKE' + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', + webhook_url='https://example.com/paddle-python', + customer_country=country, + ) + error.match('Country code "{0}" is not valid'.format(country)) + + +def test_create_pay_link_country_without_postcode(paddle_client): # NOQA: F811 + country = 'US' + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', + webhook_url='https://example.com/paddle-python', + customer_country=country, + ) + + message = ( + 'customer_postcode must be set for {0} when customer_country is set' ) + error.match(message.format(country)) + + +def test_create_pay_link_vat_number(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + ) + error.match('vat_company_name must be set if vat_number is set') + + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + vat_company_name='name', + ) + error.match('vat_street must be set if vat_number is set') + + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + vat_company_name='name', vat_street='street', + ) + error.match('vat_city must be set if vat_number is set') + + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + vat_company_name='name', vat_street='street', + vat_city='city', + ) + error.match('vat_state must be set if vat_number is set') + + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + vat_company_name='name', vat_street='street', + vat_city='city', vat_state='state', + ) + error.match('vat_country must be set if vat_number is set') + + with pytest.raises(ValueError) as error: + country = 'US' + paddle_client.create_pay_link( + title='test', webhook_url='fake', vat_number='1234', + vat_company_name='name', vat_street='street', + vat_city='city', vat_state='state', vat_country=country, + ) + message = 'vat_postcode must be set for {0} when vat_country is set' + error.match(message.format(country)) From 3e87e904425f48eeb941eb12d750f1c01ddba74b Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:38:45 +0000 Subject: [PATCH 19/30] Update CONTRIBUTING to reference sandbox env for test setup --- CONTRIBUTING.md | 101 ++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94aac24..b184b38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,83 +28,92 @@ Please see the sections below for more details 1. check all coding conventions are adhered to (`tox`) 1. Commit your changes (`git commit -am 'Add some feature'`) 1. Push to the branch (`git push origin my-new-feature`) -1. Create new pull request +1. Create a new pull request -## Setup +## Testing -This package uses the [Poetry](https://python-poetry.org/) for packaging and dependency management. Before you get started please install it. +### Setup -Once you have cloned this repository you will then need: -* Your `Paddle Vendor ID` which can be found on the [Paddle's authentication page](https://vendors.paddle.com/authentication) -* Your `Paddle API key` which is found on the same [Paddle's authentication page](https://vendors.paddle.com/authentication) +This package uses [Poetry](https://python-poetry.org/) for packaging and dependency management. Before you get started please install it. ```bash # Fork and clone this repo poetry install - -# Create a file called .env and add the above settings -export PADDLE_VENDOR_ID=... -export PADDLE_API_KEY="..." - poetry shell -source .env ``` +An account in the [Paddle Sandbox](https://sandbox-vendors.paddle.com/authentication) has been created for testing this package and this account has been hardcoded into the tests via the paddle-client fixture so all of the tests will ignore any PADDLE_* environmental variables. -## Running tests +This sandbox account is currently configured in a state that all of the tests pass out of the box including the creation of products and subscriptions which can't be done via the API. +With that in mind, this might not be the case in the future. If a test fails due to missing data, the pytest error should make it clear what data needs to be created. Below are instructions on how to create the test data. -At the moment several of the tests require a few extra bits of information from Paddle. We are looking at removing these dependencies soon be creating them with fixtures. Please help us do it if you are up for it. -* A `Paddle Product ID` which you can get one by creating a product on [Paddle's product page](https://vendors.paddle.com/products) -* A `Paddle Plan/Subscription ID` which can be created on [Paddle's Subscription Plans page](https://vendors.paddle.com/subscriptions/plans) -* A `Paddle Checkout ID` which can be got by going into an order from the [Paddle orders page](https://vendors.paddle.com/orders). If you don't have any orders yet you can create an order for $0. +#### Creating a product -The tests currently require you to add the above as environmental variables (names below). To make them easier to set these each time the python virtual environment is loaded they can be placed into a `.env` file which can be sourced. +This requires access to the Paddle Sandbox account (`paddle-client@pkgdeploy.com`). If you do not have access please create a GitHub issue. -```bash -# Add the above to the relevant environmental variables (and .env file) -export PADDLE_TEST_DEFAULT_CHECKOUT_ID="..." -export PADDLE_TEST_DEFAULT_PRODUCT_ID=... -export PADDLE_TEST_DEFAULT_PLAN_ID=... +1. Go to the Sandbox products https://sandbox-vendors.paddle.com/products +1. Product Name: `test-product` (the name is used to match the fixture) +1. Fulfillment Method: `Paddle License` + * Default Activations per License: 9999 + * Enable Trials: Unchecked + * Default Expiry Days: 0 +1. Complete your integration of the Paddle SDK: Ignore Waiting for API requests... (this can be ignored) +1. Set Prices > Go to prices manager + * USD: $1 + * Sale: Disabled +1. Close the page (without saving) -poetry shell -source .env -pytest -m "not manual_cleanup" tests/ -# Coverage info is written to htmlcov/ -pytest tests/ # Run all tests against Paddle's API. See mocking and cleanup below -``` +#### Creating a subscription -### Mocking +Certain tests require a subscription to be created, which is simply a plan that has been paid for by a user. While there is no way to create subscriptions / payment via the Paddle API, a simple way to using the PaddleCheckout has been configured to create them manually in a few seconds: -As few mocks should be used as possible, Mocks should only be used for dangerous Paddle operations that can't be undone or cleaned up. +Before following the below steps to create a payment please run the tests as a payment may already exist. It will also make sure a Paddle plan is setup ready for a subscription and let you know of the plan ID which is needed below. -Mocks should be done at the point paddle-python interfaces with `requests` and check the exact kwargs that were sent. This will cause any change in the request to cause the mocked test to fail. All mocked tests should also be accompanied by a matching test which hits Paddle's API but has the decorator`@pytest.mark.skip()` (see an already mocked test below as an example). +1. Run a test which requires a subscription payment (to print the Plan ID) - `pytest tests/test_subscription_payments.py::test_list_subscription_payments` +1. Take note of the plan ID from the failed test (if the test does not fail you don't need to setup a new subscription) +1. Edit the PaddleCheckout HTML page at `tests/create_subscription.html` replacing `data-product=""` with the output of the above command: + ``` + + + Buy Now! + + ``` +1. Open the create_subscription checkout HTML page - `open tests/create_subscription.html` +1. Click on the `Buy Now!` button +1. In the Paddle modal enter fake card info provided on the page -The current mocked tests are: -* Refund Payment - `test_transactions.py::test_refund_product_payment` -* Cancel Subscription - `test_subscription_users.py::test_cancel_subscription` -* Update Subscription - `test_subscription_users.py::test_update_subscription` -* Create one off charge - `test_one_off_charges.py::test_create_one_off_charge` +## Running tests +Pytest is used to run the tests: -### Cleanup +```bash +pytest tests/ +# Coverage info is written to htmlcov/ +``` +All tests are run against the Paddle Sandbox environment and most of the setup and teardown is handled within the tests. -_(These tests are currently not working and marked as skipped so this can be ignored)_ +The only exception to this is if someone accidentally deletes all of the subscription plans and products. When this happens it means any test which requires a checkout to have been completed (payments, updates etc) will fail due to no plan or product existing. -Parts of the Paddle API have create endpoints but not delete endpoints. Because of this several tests need to be cleaned up manually after they are run: +### Mocking -* `tests/test_licenses.py::test_generate_license` -* `tests/test_pay_links.py::test_create_pay_link` +As few mocks should be used as possible, Mocks should only be used for dangerous Paddle operations that can't be undone or cleaned up via the API making it difficult to create enough test data.. +Mocks should be done at the point paddle-python interfaces with `requests` and check the exact kwargs that were sent. This will cause any change in the request to cause the mocked test to fail. All mocked tests should also be accompanied by a matching test that hits Paddle's API but has the decorator`@pytest.mark.skip()` (see an already mocked test below as an example). + +The current mocked tests are: + +* Cancel Subscription - `test_subscription_users.py::test_cancel_subscription` -If you want to run `pytest` without running the tests that need manual clean up you can use -```bash -pytest -m "not manual_cleanup" tests/ -``` ## Coding conventions From ce358084f10358565bfaf0ab9fda4edd236ba286 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:40:39 +0000 Subject: [PATCH 20/30] Update README to remove note about the no longer broken endpoints --- README.md | 28 +++------------------------- docs/api_reference.rst | 10 ---------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 3f50075..747b861 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A python (3.5+) wrapper around the [Paddle.com API](https://developer.paddle.com/api-reference/intro) -If you are looking at intergrating Paddle with Django check out [dj-paddle](https://github.com/paddle-python/dj-paddle) +If you are looking at integrating Paddle with Django check out [dj-paddle](https://github.com/paddle-python/dj-paddle) The full documentation is available at: https://paddle-client.readthedocs.io @@ -29,7 +29,7 @@ paddle = PaddleClient(vendor_id=12345, api_key='myapikey') paddle.list_products() ``` -If `vendor_id` and `api_key` are not passed through when initalising Paddle will fall back and try and use environmental variables called `PADDLE_VENDOR_ID` and `PADDLE_API_KEY` +If `vendor_id` and `api_key` are not passed through when initialising Paddle will fall back and try and use environmental variables called `PADDLE_VENDOR_ID` and `PADDLE_API_KEY` ```bash export PADDLE_VENDOR_ID=12345 export PADDLE_API_KEY="myapikey" @@ -56,7 +56,7 @@ All contributions are welcome and appreciated. Please see [CONTRIBUTING.md](http ## Paddle Endpoints -The below endpoints from the [Paddle API Reference](https://developer.paddle.com/api-reference) have been implimented +The below endpoints from the [Paddle API Reference](https://developer.paddle.com/api-reference) have been implemented For full details see the [API Reference in the docs](https://paddle-client.readthedocs.io/en/latest/api_reference.html). This includes details on parameters and return types for all the different methods as well as other helper methods around the Paddle.com API. @@ -178,25 +178,3 @@ paddle.create_one_off_charge( # Alert API paddle.get_webhook_history() ``` - - -## Failing Endpoints - -The below endpoints have been implimented but are not working correctly according to the tests. They have been commented out in `paddle/paddle.py` and the tests will skip is the methods do not exist - -* [Generate License](https://developer.paddle.com/api-reference/product-api/licenses/createlicense) - `Paddle error 108 - Unable to find requested product` -* [Create pay link](https://developer.paddle.com/api-reference/product-api/pay-links/createpaylink) - `Paddle error 108 - Unable to find requested product` -* [Reschedule subscription payment](https://developer.paddle.com/api-reference/subscription-api/payments/updatepayment) - `Paddle error 122 - Provided date is not valid` - After manually testing via Paddles API reference I believe this is an issue with Paddle's API. - - -## ToDo -* Fix generate license, create pay link and reschedule payment endpoints -* Get test coverage to 100% -* Use `pytest-mock` `Spy` to check params, json, urls etc for test requests - * Needed to any tests which skip due to missing data -* How to deal with the manual cleanup? -* Pull request template -* TravisCI? -* Dependabot -* Remove double call for exception error message checking - How to get the exception str from `pytest.raises()`? pytest-mock `Spy`? -* Add pytest warnings to provide direct links to Paddle for bits that need to be cleaned up diff --git a/docs/api_reference.rst b/docs/api_reference.rst index ec96d3d..859ac7f 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -49,16 +49,6 @@ As listed in the `Paddle API Reference `_ - ``Paddle error 108 - Unable to find requested product`` -- `Create pay link `_ - ``Paddle error 108 - Unable to find requested product`` -- `Reschedule subscription payment `_ - ``Paddle error 122 - Provided date is not valid`` - - Full reference -------------- From eabf58d2d48798f20707b61c1ca4406e409ed025 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 03:41:32 +0000 Subject: [PATCH 21/30] Bump to 1.0.0 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ea33682..f5cbdef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "paddle-client" packages = [ {include = "paddle"} ] -version = "0.8.1" +version = "1.0.0" description = "Python wrapper around the Paddle.com API" license = "MIT" authors = ["Matt Pye "] @@ -13,7 +13,7 @@ homepage = "https://github.com/paddle-python/paddle-client" keywords = ["paddle", "paddle.com", "payments", "billing", "commerce", "finance", "saas"] [tool.poetry.dependencies] -python = "^3.5.2" +python = "^3.5.0" requests = "^2.23.0" [tool.poetry.dev-dependencies] From ccdbb270173e7decb6530b1f25ca7afed712c670 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 20:11:31 +0000 Subject: [PATCH 22/30] Test coupon errors --- paddle/_coupons.py | 4 +- tests/test_coupons.py | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/paddle/_coupons.py b/paddle/_coupons.py index eef0b91..47b44df 100644 --- a/paddle/_coupons.py +++ b/paddle/_coupons.py @@ -53,10 +53,8 @@ def create_coupon( raise ValueError('product_ids must be specified if coupon_type is "product"') # NOQA: E501 if discount_type not in ['flat', 'percentage']: raise ValueError('coupon_type must be "product" or "checkout"') - if discount_type == 'flat' and not currency: - raise ValueError('currency must be specified if discount_type is "flat"') # NOQA: E501 if coupon_code and (coupon_prefix or num_coupons): - raise ValueError('coupon_prefix and num_coupons not valid when coupon_code set') # NOQA: E501 + raise ValueError('coupon_prefix and num_coupons are not valid when coupon_code set') # NOQA: E501 json = { 'coupon_type': coupon_type, diff --git a/tests/test_coupons.py b/tests/test_coupons.py index 46be566..f82b51e 100644 --- a/tests/test_coupons.py +++ b/tests/test_coupons.py @@ -86,6 +86,89 @@ def test_create_coupon(paddle_client, get_product): # NOQA: F811 assert found +def test_create_coupon_invalid_coupon_type(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='test', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='USD' + ) + error.match('coupon_type must be "product" or "checkout"') + + +def test_create_coupon_missing_product_ids(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='product', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='USD' + ) + error.match('product_ids must be specified if coupon_type is "product"') + + +def test_create_coupon_bad_discount_type(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='checkout', + discount_type='test', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='USD' + ) + error.match('coupon_type must be "product" or "checkout"') + + +def test_create_coupon_code_with_coupon_prefix(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='checkout', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='USD', + coupon_code='test', + coupon_prefix='test' + ) + error.match('coupon_prefix and num_coupons are not valid when coupon_code set') # NOQA: E501 + + +def test_create_coupon_code_with_num_coupons(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='checkout', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='USD', + coupon_code='test', + num_coupons=10, + ) + error.match('coupon_prefix and num_coupons are not valid when coupon_code set') # NOQA: E501 + + +def test_create_coupon_invalid_currency(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_coupon( + coupon_type='checkout', + discount_type='percentage', + discount_amount=1, + allowed_uses=1, + recurring=False, + currency='TEST', + + ) + error.match('currency must be a 3 letter currency code') + + def test_delete_coupon(paddle_client, create_coupon): # NOQA: F811 coupon_code, product_id = create_coupon @@ -139,3 +222,23 @@ def test_update_coupon(paddle_client, create_coupon): # NOQA: F811 # The discount_currency is returned as None # assert coupon['discount_currency'] == currency assert found + + +def test_update_coupon_code_and_group(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.update_coupon( + coupon_code='coupon_code', + group='group' + + ) + error.match('You must specify either coupon_code or group, but not both') + + +def test_update_coupon_invalid_currency(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.update_coupon( + coupon_code='coupon_code', + currency='TEST', + + ) + error.match('currency must be a 3 letter currency code') From be7e55ba64a0e940aebd4fe527cc6631d88362b3 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:40:04 +0000 Subject: [PATCH 23/30] Test validators --- paddle/validators.py | 4 ++-- tests/test_validators.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/test_validators.py diff --git a/paddle/validators.py b/paddle/validators.py index 05f0907..8efb045 100644 --- a/paddle/validators.py +++ b/paddle/validators.py @@ -5,8 +5,8 @@ def validate_date(value: DateType, field_name: str) -> str: date_format = '%Y-%m-%d' + error_message = '{0} must be a datetime/date object or string in format YYYY-MM-DD'.format(field_name) # NOQA: E501 if isinstance(value, str): - error_message = '{0} must be a datetime/data object or string in format YYYY-MM-DD'.format(field_name) # NOQA: E501 try: datetime.strptime(value, date_format) except ValueError: @@ -21,8 +21,8 @@ def validate_date(value: DateType, field_name: str) -> str: def validate_datetime(value: DatetimeType, field_name: str) -> str: datetime_format = '%Y-%m-%d %H:%M:%S' + error_message = '{0} must be a datetime object or string in format YYYY-MM-DD HH:MM:SS'.format(field_name) # NOQA: E501 if isinstance(value, str): - error_message = '{0} must be a datetime object or string in format YYYY-MM-DD HH:MM:SS'.format(field_name) # NOQA: E501 try: datetime.strptime(value, datetime_format) except ValueError: diff --git a/tests/test_validators.py b/tests/test_validators.py new file mode 100644 index 0000000..0d7d503 --- /dev/null +++ b/tests/test_validators.py @@ -0,0 +1,27 @@ +import pytest + +from paddle.validators import validate_date, validate_datetime + + +def test_validate_date_not_string_or_date(): + field_name = 'test' + with pytest.raises(ValueError) as error: + validate_date(value=123, field_name=field_name) + msg = '{0} must be a datetime/date object or string in format YYYY-MM-DD' + error.match(msg.format(field_name)) + + +def test_validate_datetime_not_string_or_datetime(): + field_name = 'test' + with pytest.raises(ValueError) as error: + validate_datetime(value=123, field_name=field_name) + msg = '{0} must be a datetime object or string in format YYYY-MM-DD' + error.match(msg.format(field_name)) + + +def test_validate_datetime_not_valid_string(): + field_name = 'test' + with pytest.raises(ValueError) as error: + validate_datetime(value='123', field_name=field_name) + msg = '{0} must be a datetime object or string in format YYYY-MM-DD' + error.match(msg.format(field_name)) From 0fe354a3a39c23e0f2a04d7031616d4bc99aa7ff Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:40:33 +0000 Subject: [PATCH 24/30] Test invalid url --- paddle/paddle.py | 2 +- tests/test_paddle.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/paddle/paddle.py b/paddle/paddle.py index 898b0ae..80deac4 100644 --- a/paddle/paddle.py +++ b/paddle/paddle.py @@ -124,7 +124,7 @@ def request( if data and json: raise ValueError('Please set either data or json not both') - if kwargs['method'] == 'GET' and (data or json): + if kwargs['method'] == 'GET' and (data or json): # pragma: no cover log.warn('GET data/json should not be provided with GET method.') if kwargs['method'] in ['POST', 'PUT', 'PATCH']: diff --git a/tests/test_paddle.py b/tests/test_paddle.py index 7174788..c2fb02a 100644 --- a/tests/test_paddle.py +++ b/tests/test_paddle.py @@ -114,6 +114,14 @@ def test_json_and_data(paddle_client): # NOQA: F811 error.match('Please set either data or json not both') +def test_invalid_url(paddle_client): # NOQA: F811 + url = 'https://example.com' + with pytest.raises(ValueError) as error: + paddle_client.get(url) + message = 'URL does not appear to be a Paddle API URL - {0}' + error.match(message.format(url)) + + def test_bad_url(paddle_client): # NOQA: F811 with pytest.raises(PaddleException) as error: with pytest.warns(RuntimeWarning) as warning: From d93f98065406798f4811ef53523dc2490e1e31af Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:41:00 +0000 Subject: [PATCH 25/30] test_list_transactions_bad_entity --- tests/test_transactions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index b883390..a98f885 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1,5 +1,7 @@ from datetime import datetime +import pytest + from .fixtures import ( # NOQA: F401 create_plan, get_checkout, get_product, get_subscription, paddle_client ) @@ -85,5 +87,12 @@ def test_list_transactions_checkout(paddle_client, get_checkout): # NOQA: F811 assert isinstance(checkout['receipt_url'], str) +def test_list_transactions_bad_entity(paddle_client): # NOQA: F811 + entity = 'test' + with pytest.raises(ValueError) as error: + paddle_client.list_transactions(entity=entity, entity_id=1) + msg = 'entity "{0}" must be one of user, subscription, order, checkout, product' # NOQA: E501 + error.match(msg.format(entity)) + # ToDo: Check order entity # ToDo: Check user entity From 8db91759bf0480fc6294513acaf0c531bb7ad747 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:41:51 +0000 Subject: [PATCH 26/30] Test update subscription value errors --- paddle/_subscription_users.py | 8 ++++++-- tests/test_subscription_users.py | 30 ++++++++++++------------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/paddle/_subscription_users.py b/paddle/_subscription_users.py index 01c93ea..ab6c0f9 100644 --- a/paddle/_subscription_users.py +++ b/paddle/_subscription_users.py @@ -92,7 +92,9 @@ def pause_subscription(self, subscription_id: int) -> dict: convenient helper method for update_subscription as no extra data can be sent when pausing/resuming subscriptions """ # NOQA: E501 - return self.update_subscription(subscription_id=subscription_id, pause=True) # NOQA: E501 + return self.update_subscription( # pragma: no cover + subscription_id=subscription_id, pause=True + ) def resume_subscription(self, subscription_id: int) -> dict: @@ -103,4 +105,6 @@ def resume_subscription(self, subscription_id: int) -> dict: convenient helper method for update_subscription as no extra data can be sent when pausing/resuming subscriptions """ # NOQA: E501 - return self.update_subscription(subscription_id=subscription_id, pause=False) # NOQA: E501 + return self.update_subscription( # pragma: no cover + subscription_id=subscription_id, pause=False + ) diff --git a/tests/test_subscription_users.py b/tests/test_subscription_users.py index bd9e457..1a32a2f 100644 --- a/tests/test_subscription_users.py +++ b/tests/test_subscription_users.py @@ -7,7 +7,6 @@ def test_list_subscription_users(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here subscription_users = paddle_client.list_subscription_users() for subscription in subscription_users: assert isinstance(subscription['subscription_id'], int) @@ -27,7 +26,6 @@ def test_list_subscription_users(paddle_client, get_subscription): # NOQA: F811 def test_list_subscription_users_with_subscription_id(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here subscription_id = get_subscription['subscription_id'] subscription_users = paddle_client.list_subscription_users( subscription_id=subscription_id, @@ -37,8 +35,6 @@ def test_list_subscription_users_with_subscription_id(paddle_client, get_subscri def test_list_subscription_users_with_plan_id(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here - plan_id = get_subscription['plan_id'] subscription_users = paddle_client.list_subscription_users(plan_id=plan_id) for subscription in subscription_users: @@ -46,7 +42,6 @@ def test_list_subscription_users_with_plan_id(paddle_client, get_subscription): def test_list_subscription_users_with_state(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here state = get_subscription['state'] subscription_users = paddle_client.list_subscription_users(state=state) for subscription in subscription_users: @@ -54,7 +49,6 @@ def test_list_subscription_users_with_state(paddle_client, get_subscription): # def test_list_subscription_users_with_page(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here list_one = paddle_client.list_subscription_users( results_per_page=1, page=1, ) @@ -65,24 +59,16 @@ def test_list_subscription_users_with_page(paddle_client, get_subscription): # def test_list_subscription_users_with_results_per_page(paddle_client, get_subscription): # NOQA: F811,E501 - # ToDo: Create plan when API exists for it here list_one = paddle_client.list_subscription_users( results_per_page=1, page=1, ) assert len(list_one) == 1 -def test_update_subscription(paddle_client, get_subscription): # NOQA: F811 - subscription_id = get_subscription['subscription_id'] - new_quantity = get_subscription['quantity'] + 0.01 - paddle_client.update_subscription( - subscription_id=subscription_id, - quantity=new_quantity, - ) - new_subscription_data = paddle_client.list_subscription_users( - subscription_id=subscription_id, - ) - assert new_subscription_data.quantity == new_quantity +def test_list_subscription_users_invalid_state(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.list_subscription_users(state='test') + error.match('state must be one of active, past due, trialling, paused') def test_update_subscription(paddle_client, get_subscription): # NOQA: F811 @@ -146,6 +132,14 @@ def test_update_subscription(paddle_client, get_subscription): # NOQA: F811 assert 'paused_reason' not in new_subscription_data +def test_update_subscription_invalid_currency(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.update_subscription( + subscription_id=1, currency='test' + ) + error.match('currency must be one of USD, GBP, EUR') + + @pytest.mark.mocked def test_cancel_subscription(mocker, paddle_client): # NOQA: F811 """ From 118419a03db928c617de7944458a20ca6760877f Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:42:19 +0000 Subject: [PATCH 27/30] Add space in list_transactions value error --- paddle/_transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paddle/_transactions.py b/paddle/_transactions.py index c2c2ed2..af06f8b 100644 --- a/paddle/_transactions.py +++ b/paddle/_transactions.py @@ -17,7 +17,7 @@ def list_transactions( valid_entities = ['user', 'subscription', 'order', 'checkout', 'product'] if entity not in valid_entities: error = 'entity "{0}" must be one of {1}'.format( - entity, ",".join(valid_entities) + entity, ', '.join(valid_entities) ) raise ValueError(error) From b1d35013dab8d7d7687148a2961ab2bac9bd510f Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:43:02 +0000 Subject: [PATCH 28/30] Test create_paylink value errors --- tests/test_pay_links.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pay_links.py b/tests/test_pay_links.py index 8fe7c09..f499769 100644 --- a/tests/test_pay_links.py +++ b/tests/test_pay_links.py @@ -49,6 +49,14 @@ def test_create_pay_link_no_product_or_webhook(paddle_client): # NOQA: F811 error.match('webhook_url must be set if product_id is not set') +def test_create_pay_link_no_product_and_recurring_prices(paddle_client): # NOQA: F811,E501 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', webhook_url='test', recurring_prices=['USD:19.99'], + ) + error.match('recurring_prices can only be set if product_id is set to a subsciption') # NOQA: F811,E501 + + def test_create_pay_link_no_product_reccuring(paddle_client): # NOQA: F811 with pytest.raises(ValueError) as error: paddle_client.create_pay_link( @@ -139,3 +147,13 @@ def test_create_pay_link_vat_number(paddle_client): # NOQA: F811 ) message = 'vat_postcode must be set for {0} when vat_country is set' error.match(message.format(country)) + + +def test_create_pay_link_invalid_date(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_pay_link( + title='test', + webhook_url='https://example.com/paddle-python', + expires='test' + ) + error.match('expires must be a datetime/date object or string in format YYYY-MM-DD') # NOQA: E501 From 0c4ec752895aca65bedde3740703ca49e8635a50 Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Tue, 23 Mar 2021 21:43:16 +0000 Subject: [PATCH 29/30] Test create_plan value errors --- tests/test_plans.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_plans.py b/tests/test_plans.py index 6b2a057..c45a79d 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -122,5 +122,28 @@ def test_create_plan_missing_price(paddle_client, currency, missing_field): # N with pytest.raises(ValueError) as error: paddle_client.create_plan(**plan) - message = r'main_currency_code is {0} so {1} must be set' + message = 'main_currency_code is {0} so {1} must be set' error.match(message.format(currency, missing_field)) + + +def test_create_plan_invalid_plan_type(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_plan( + plan_name='test', + plan_type='test', + plan_trial_days=0, + plan_length=999, + ) + error.match('plan_type must be one of day, week, month, year') + + +def test_create_plan_invalid_main_currency_code(paddle_client): # NOQA: F811 + with pytest.raises(ValueError) as error: + paddle_client.create_plan( + plan_name='plan_name', + plan_trial_days=0, + plan_type='year', + plan_length=999, + main_currency_code='test' + ) + error.match('main_currency_code must be one of USD, GBP, EUR') From d8db0657daf73fe32128f321b8977c90550c71ea Mon Sep 17 00:00:00 2001 From: Matt Pye Date: Fri, 26 Mar 2021 01:25:49 +0000 Subject: [PATCH 30/30] Add sandbox info to README and docs --- README.md | 24 ++++++++++++++++++++++++ docs/getting_started.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/README.md b/README.md index 747b861..3bca148 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,30 @@ paddle.list_products() ``` +### Paddle sandbox environment + +The [Paddle sandbox environment](https://developer.paddle.com/getting-started/sandbox) is a separate Paddle environment which can be used for development and testing. You are required to create a new account in this environment, different to your production account. + +Once you have this account setup and configured you can user the sandbox account by passing `sandbox=True` when initialising the Paddle Client. This will send all API calls to the Paddle sandbox URLs instead of the production URLs + +```python +from paddle import PaddleClient + + +paddle = PaddleClient(vendor_id=12345, api_key='myapikey', sandbox=True) +``` + +It is also possible to turn the sandbox environment on using an environmental variable called `PADDLE_SANDBOX`: +```bash +export PADDLE_SANDBOX="true" +``` +```python +from paddle import PaddleClient + + +paddle = PaddleClient(vendor_id=12345, api_key='myapikey') +``` + ## Documentation The full documentation is available on Read the Docs: https://paddle-client.readthedocs.io diff --git a/docs/getting_started.rst b/docs/getting_started.rst index ab62ceb..ef1f1d3 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -48,3 +48,32 @@ If ``vendor_id`` and ``api_key`` are not passed through when initalising Paddle paddle = PaddleClient() paddle.list_products() + + +Paddle sandbox environment +-------------------------- + +The `Paddle sandbox environment `_ is a separate Paddle environment which can be used for development and testing. You are required to create a new account in this environment, different to your production account. + +Once you have this account setup and configured you can user the sandbox account by passing ``sandbox=True`` when initialising the Paddle Client. This will send all API calls to the Paddle sandbox URLs instead of the production URLs + + +.. code-block:: python + + from paddle import PaddleClient + + paddle = PaddleClient(vendor_id=12345, api_key='myapikey', sandbox=True) + + +It is also possible to turn the sandbox environment on using an environmental variable called ``PADDLE_SANDBOX``: + +.. code-block:: bash + + export PADDLE_SANDBOX="true" + + +.. code-block:: python + + from paddle import PaddleClient + + paddle = PaddleClient(vendor_id=12345, api_key='myapikey')