Skip to content

Commit 4a5e07d

Browse files
authored
Merge pull request #6 from paddle-python/sandbox-tests
Use the Paddle sandbox environment for testing
2 parents 2fe7eb9 + d8db065 commit 4a5e07d

34 files changed

+1080
-1009
lines changed

CONTRIBUTING.md

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -28,83 +28,92 @@ Please see the sections below for more details
2828
1. check all coding conventions are adhered to (`tox`)
2929
1. Commit your changes (`git commit -am 'Add some feature'`)
3030
1. Push to the branch (`git push origin my-new-feature`)
31-
1. Create new pull request
31+
1. Create a new pull request
3232

3333

34-
## Setup
34+
## Testing
3535

36-
This package uses the [Poetry](https://python-poetry.org/) for packaging and dependency management. Before you get started please install it.
36+
### Setup
3737

38-
Once you have cloned this repository you will then need:
39-
* Your `Paddle Vendor ID` which can be found on the [Paddle's authentication page](https://vendors.paddle.com/authentication)
40-
* Your `Paddle API key` which is found on the same [Paddle's authentication page](https://vendors.paddle.com/authentication)
38+
This package uses [Poetry](https://python-poetry.org/) for packaging and dependency management. Before you get started please install it.
4139

4240
```bash
4341
# Fork and clone this repo
4442
poetry install
45-
46-
# Create a file called .env and add the above settings
47-
export PADDLE_VENDOR_ID=...
48-
export PADDLE_API_KEY="..."
49-
5043
poetry shell
51-
source .env
5244
```
5345

46+
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.
5447

55-
## Running tests
48+
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.
49+
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.
5650

57-
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.
5851

59-
* A `Paddle Product ID` which you can get one by creating a product on [Paddle's product page](https://vendors.paddle.com/products)
60-
* A `Paddle Plan/Subscription ID` which can be created on [Paddle's Subscription Plans page](https://vendors.paddle.com/subscriptions/plans)
61-
* 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.
52+
#### Creating a product
6253

63-
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.
54+
This requires access to the Paddle Sandbox account (`[email protected]`). If you do not have access please create a GitHub issue.
6455

65-
```bash
66-
# Add the above to the relevant environmental variables (and .env file)
67-
export PADDLE_TEST_DEFAULT_CHECKOUT_ID="..."
68-
export PADDLE_TEST_DEFAULT_PRODUCT_ID=...
69-
export PADDLE_TEST_DEFAULT_PLAN_ID=...
56+
1. Go to the Sandbox products https://sandbox-vendors.paddle.com/products
57+
1. Product Name: `test-product` (the name is used to match the fixture)
58+
1. Fulfillment Method: `Paddle License`
59+
* Default Activations per License: 9999
60+
* Enable Trials: Unchecked
61+
* Default Expiry Days: 0
62+
1. Complete your integration of the Paddle SDK: Ignore Waiting for API requests... (this can be ignored)
63+
1. Set Prices > Go to prices manager
64+
* USD: $1
65+
* Sale: Disabled
66+
1. Close the page (without saving)
7067

71-
poetry shell
72-
source .env
73-
pytest -m "not manual_cleanup" tests/
74-
# Coverage info is written to htmlcov/
7568

76-
pytest tests/ # Run all tests against Paddle's API. See mocking and cleanup below
77-
```
69+
#### Creating a subscription
7870

79-
### Mocking
71+
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:
8072

81-
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.
73+
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.
8274

83-
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).
75+
1. Run a test which requires a subscription payment (to print the Plan ID) - `pytest tests/test_subscription_payments.py::test_list_subscription_payments`
76+
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)
77+
1. Edit the PaddleCheckout HTML page at `tests/create_subscription.html` replacing `data-product="<plan-id>"` with the output of the above command:
78+
```
79+
<!-- If the new plan ID was 9000: -->
80+
<a
81+
data-product="9000"
82+
class="paddle_button"
83+
href="#!"
84+
...
85+
>
86+
Buy Now!
87+
</a>
88+
```
89+
1. Open the create_subscription checkout HTML page - `open tests/create_subscription.html`
90+
1. Click on the `Buy Now!` button
91+
1. In the Paddle modal enter fake card info provided on the page
8492
85-
The current mocked tests are:
8693
87-
* Refund Payment - `test_transactions.py::test_refund_product_payment`
88-
* Cancel Subscription - `test_subscription_users.py::test_cancel_subscription`
89-
* Update Subscription - `test_subscription_users.py::test_update_subscription`
90-
* Create one off charge - `test_one_off_charges.py::test_create_one_off_charge`
94+
## Running tests
9195
96+
Pytest is used to run the tests:
9297
93-
### Cleanup
98+
```bash
99+
pytest tests/
100+
# Coverage info is written to htmlcov/
101+
```
102+
All tests are run against the Paddle Sandbox environment and most of the setup and teardown is handled within the tests.
94103

95-
_(These tests are currently not working and marked as skipped so this can be ignored)_
104+
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.
96105

97-
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:
98106

107+
### Mocking
99108

100-
* `tests/test_licenses.py::test_generate_license`
101-
* `tests/test_pay_links.py::test_create_pay_link`
109+
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..
102110

111+
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).
112+
113+
The current mocked tests are:
114+
115+
* Cancel Subscription - `test_subscription_users.py::test_cancel_subscription`
103116

104-
If you want to run `pytest` without running the tests that need manual clean up you can use
105-
```bash
106-
pytest -m "not manual_cleanup" tests/
107-
```
108117

109118
## Coding conventions
110119

README.md

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A python (3.5+) wrapper around the [Paddle.com API](https://developer.paddle.com/api-reference/intro)
44

5-
If you are looking at intergrating Paddle with Django check out [dj-paddle](https://github.com/paddle-python/dj-paddle)
5+
If you are looking at integrating Paddle with Django check out [dj-paddle](https://github.com/paddle-python/dj-paddle)
66

77
The full documentation is available at: https://paddle-client.readthedocs.io
88

@@ -29,7 +29,7 @@ paddle = PaddleClient(vendor_id=12345, api_key='myapikey')
2929
paddle.list_products()
3030
```
3131

32-
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`
32+
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`
3333
```bash
3434
export PADDLE_VENDOR_ID=12345
3535
export PADDLE_API_KEY="myapikey"
@@ -44,6 +44,30 @@ paddle.list_products()
4444
```
4545

4646

47+
### Paddle sandbox environment
48+
49+
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.
50+
51+
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
52+
53+
```python
54+
from paddle import PaddleClient
55+
56+
57+
paddle = PaddleClient(vendor_id=12345, api_key='myapikey', sandbox=True)
58+
```
59+
60+
It is also possible to turn the sandbox environment on using an environmental variable called `PADDLE_SANDBOX`:
61+
```bash
62+
export PADDLE_SANDBOX="true"
63+
```
64+
```python
65+
from paddle import PaddleClient
66+
67+
68+
paddle = PaddleClient(vendor_id=12345, api_key='myapikey')
69+
```
70+
4771
## Documentation
4872

4973
The full documentation is available on Read the Docs: https://paddle-client.readthedocs.io
@@ -56,7 +80,7 @@ All contributions are welcome and appreciated. Please see [CONTRIBUTING.md](http
5680

5781
## Paddle Endpoints
5882

59-
The below endpoints from the [Paddle API Reference](https://developer.paddle.com/api-reference) have been implimented
83+
The below endpoints from the [Paddle API Reference](https://developer.paddle.com/api-reference) have been implemented
6084

6185
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.
6286

@@ -82,7 +106,6 @@ See [`Usage`](#usage) below for quick examples.
82106
* [List Subscription Users](https://developer.paddle.com/api-reference/subscription-api/subscription-users/listusers)
83107
* [Cancel Subscription](https://developer.paddle.com/api-reference/subscription-api/subscription-users/canceluser)
84108
* [Update Subscription](https://developer.paddle.com/api-reference/subscription-api/subscription-users/updateuser)
85-
* [Preview Subscription Update](https://developer.paddle.com/api-reference/subscription-api/subscription-users/previewupdate)
86109
* [Add Modifier](https://developer.paddle.com/api-reference/subscription-api/modifiers/createmodifier)
87110
* [Delete Modifier](https://developer.paddle.com/api-reference/subscription-api/modifiers/deletemodifier)
88111
* [List Modifiers](https://developer.paddle.com/api-reference/subscription-api/modifiers/listmodifiers)
@@ -93,6 +116,7 @@ See [`Usage`](#usage) below for quick examples.
93116
**Alert API**
94117
* [Get Webhook History](https://developer.paddle.com/api-reference/alert-api/webhooks/webhooks)
95118

119+
96120
### Usage
97121

98122
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 +188,6 @@ paddle.update_subscription(
164188
)
165189
paddle.pause_subscription(subscription_id=1234)
166190
paddle.resume_subscription(subscription_id=1234)
167-
paddle.preview_update_subscription(
168-
subscription_id=123,
169-
bill_immediately=True,
170-
quantity=101,
171-
)
172191
paddle.add_modifier(subscription_id=1234, modifier_amount=10.5)
173192
paddle.delete_modifier(modifier_id=10)
174193
paddle.list_modifiers()
@@ -183,25 +202,3 @@ paddle.create_one_off_charge(
183202
# Alert API
184203
paddle.get_webhook_history()
185204
```
186-
187-
188-
## Failing Endpoints
189-
190-
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
191-
192-
* [Generate License](https://developer.paddle.com/api-reference/product-api/licenses/createlicense) - `Paddle error 108 - Unable to find requested product`
193-
* [Create pay link](https://developer.paddle.com/api-reference/product-api/pay-links/createpaylink) - `Paddle error 108 - Unable to find requested product`
194-
* [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.
195-
196-
197-
## ToDo
198-
* Fix generate license, create pay link and reschedule payment endpoints
199-
* Get test coverage to 100%
200-
* Use `pytest-mock` `Spy` to check params, json, urls etc for test requests
201-
* Needed to any tests which skip due to missing data
202-
* How to deal with the manual cleanup?
203-
* Pull request template
204-
* TravisCI?
205-
* Dependabot
206-
* Remove double call for exception error message checking - How to get the exception str from `pytest.raises()`? pytest-mock `Spy`?
207-
* Add pytest warnings to provide direct links to Paddle for bits that need to be cleaned up

docs/api_reference.rst

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ As listed in the `Paddle API Reference <https://developer.paddle.com/api-referen
3636
- :meth:`List Subscription Users<paddle.PaddleClient.list_subscription_users>`
3737
- :meth:`Cancel Subscription<paddle.PaddleClient.cancel_subscription>`
3838
- :meth:`Update Subscription<paddle.PaddleClient.update_subscription>` - (Including :meth:`Pause Subscription<paddle.PaddleClient.pause_subscription>` and :meth:`Resume Subscription<paddle.PaddleClient.resume_subscription>`)
39-
- :meth:`Preview Subscription Update<paddle.PaddleClient.preview_subscription_update>`
4039
- :meth:`Add Modifier<paddle.PaddleClient.add_modifier>`
4140
- :meth:`Delete Modifier<paddle.PaddleClient.delete_modifier>`
4241
- :meth:`List Modifiers<paddle.PaddleClient.list_modifiers>`
@@ -50,16 +49,6 @@ As listed in the `Paddle API Reference <https://developer.paddle.com/api-referen
5049

5150

5251

53-
Broken endpoints
54-
----------------
55-
56-
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
57-
58-
- `Generate License <https://developer.paddle.com/api-reference/product-api/licenses/createlicense>`_ - ``Paddle error 108 - Unable to find requested product``
59-
- `Create pay link <https://developer.paddle.com/api-reference/product-api/pay-links/createpaylink>`_ - ``Paddle error 108 - Unable to find requested product``
60-
- `Reschedule subscription payment <https://developer.paddle.com/api-reference/subscription-api/payments/updatepayment>`_ - ``Paddle error 122 - Provided date is not valid``
61-
62-
6352
Full reference
6453
--------------
6554

docs/getting_started.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,32 @@ If ``vendor_id`` and ``api_key`` are not passed through when initalising Paddle
4848
4949
paddle = PaddleClient()
5050
paddle.list_products()
51+
52+
53+
Paddle sandbox environment
54+
--------------------------
55+
56+
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.
57+
58+
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
59+
60+
61+
.. code-block:: python
62+
63+
from paddle import PaddleClient
64+
65+
paddle = PaddleClient(vendor_id=12345, api_key='myapikey', sandbox=True)
66+
67+
68+
It is also possible to turn the sandbox environment on using an environmental variable called ``PADDLE_SANDBOX``:
69+
70+
.. code-block:: bash
71+
72+
export PADDLE_SANDBOX="true"
73+
74+
75+
.. code-block:: python
76+
77+
from paddle import PaddleClient
78+
79+
paddle = PaddleClient(vendor_id=12345, api_key='myapikey')

paddle/_coupons.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,8 @@ def create_coupon(
5353
raise ValueError('product_ids must be specified if coupon_type is "product"') # NOQA: E501
5454
if discount_type not in ['flat', 'percentage']:
5555
raise ValueError('coupon_type must be "product" or "checkout"')
56-
if discount_type == 'flat' and not currency:
57-
raise ValueError('currency must be specified if discount_type is "flat"') # NOQA: E501
5856
if coupon_code and (coupon_prefix or num_coupons):
59-
raise ValueError('coupon_prefix and num_coupons not valid when coupon_code set') # NOQA: E501
57+
raise ValueError('coupon_prefix and num_coupons are not valid when coupon_code set') # NOQA: E501
6058

6159
json = {
6260
'coupon_type': coupon_type,

paddle/_licenses.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def generate_license(
1414
expires_at: DatetimeType = None,
1515
) -> dict:
1616
"""
17-
1817
`Generate License Paddle docs <https://developer.paddle.com/api-reference/product-api/licenses/createlicense>`_
1918
""" # NOQA: E501
2019
url = urljoin(self.vendors_v2, 'product/generate_license')

paddle/_pay_links.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def create_pay_link(
2727
expires: DatetimeType = None,
2828
affiliates: List[str] = None,
2929
recurring_affiliate_limit: int = None,
30-
marketing_consent: str = None,
30+
marketing_consent: bool = None,
3131
customer_email: str = None,
3232
customer_country: str = None,
3333
customer_postcode: str = None,
@@ -47,13 +47,16 @@ def create_pay_link(
4747
4848
Paddle error 108 - Unable to find requested product
4949
50-
5150
Even though the docs states:
5251
5352
"If no product_id is set, custom non-subscription product checkouts
5453
can be generated instead by specifying title, webhook_url and prices."
54+
55+
Sending an invalid coupon code will result in the request failing with
56+
"Paddle error 101 - Bad method call"
57+
5558
""" # NOQA: E501
56-
url = urljoin(self.vendors_v2, 'product/generate_license')
59+
url = urljoin(self.vendors_v2, 'product/generate_pay_link')
5760

5861
if not product_id:
5962
if not title:
@@ -62,14 +65,16 @@ def create_pay_link(
6265
raise ValueError('webhook_url must be set if product_id is not set') # NOQA: E501
6366
if recurring_prices:
6467
raise ValueError('recurring_prices can only be set if product_id is set to a subsciption') # NOQA: E501
68+
if webhook_url and product_id:
69+
raise ValueError('product_id and webhook_url cannot both be set') # NOQA: E501
6570
if customer_country:
6671
if customer_country not in supported_countries.keys():
6772
error = 'Country code "{0}" is not valid'.format(customer_country)
6873
raise ValueError(error)
6974
if customer_country in countries_requiring_postcode and not customer_postcode: # NOQA: E501
7075
error = ('customer_postcode must be set for {0} when '
71-
'customer_country is set'.format(vat_country))
72-
raise ValueError(error)
76+
'customer_country is set')
77+
raise ValueError(error.format(customer_country))
7378

7479
if vat_number:
7580
if not vat_company_name:
@@ -103,7 +108,7 @@ def create_pay_link(
103108
'quantity': quantity,
104109
'affiliates': affiliates,
105110
'recurring_affiliate_limit': recurring_affiliate_limit,
106-
'marketing_consent': marketing_consent,
111+
'marketing_consent': '1' if marketing_consent else '0',
107112
'customer_email': customer_email,
108113
'customer_country': customer_country,
109114
'customer_postcode': customer_postcode,

paddle/_subscription_payments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Union
33
from urllib.parse import urljoin
44

5-
from .types import DateType, PaddleJsonType
5+
from .types import DateType, PaddleJsonType # NOQA: F401
66
from .validators import validate_date
77

88
log = logging.getLogger(__name__)

0 commit comments

Comments
 (0)