Skip to content

Commit

Permalink
Merge pull request #6 from paddle-python/sandbox-tests
Browse files Browse the repository at this point in the history
Use the Paddle sandbox environment for testing
  • Loading branch information
pyepye committed Mar 26, 2021
2 parents 2fe7eb9 + d8db065 commit 4a5e07d
Show file tree
Hide file tree
Showing 34 changed files with 1,080 additions and 1,009 deletions.
101 changes: 55 additions & 46 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="<plan-id>"` with the output of the above command:
```
<!-- If the new plan ID was 9000: -->
<a
data-product="9000"
class="paddle_button"
href="#!"
...
>
Buy Now!
</a>
```
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

Expand Down
59 changes: 28 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand All @@ -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
Expand All @@ -56,7 +80,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.

Expand All @@ -82,7 +106,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)
Expand All @@ -93,6 +116,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.
Expand Down Expand Up @@ -164,11 +188,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()
Expand All @@ -183,25 +202,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
11 changes: 0 additions & 11 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ As listed in the `Paddle API Reference <https://developer.paddle.com/api-referen
- :meth:`List Subscription Users<paddle.PaddleClient.list_subscription_users>`
- :meth:`Cancel Subscription<paddle.PaddleClient.cancel_subscription>`
- :meth:`Update Subscription<paddle.PaddleClient.update_subscription>` - (Including :meth:`Pause Subscription<paddle.PaddleClient.pause_subscription>` and :meth:`Resume Subscription<paddle.PaddleClient.resume_subscription>`)
- :meth:`Preview Subscription Update<paddle.PaddleClient.preview_subscription_update>`
- :meth:`Add Modifier<paddle.PaddleClient.add_modifier>`
- :meth:`Delete Modifier<paddle.PaddleClient.delete_modifier>`
- :meth:`List Modifiers<paddle.PaddleClient.list_modifiers>`
Expand All @@ -50,16 +49,6 @@ As listed in the `Paddle API Reference <https://developer.paddle.com/api-referen



Broken 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``


Full reference
--------------

Expand Down
29 changes: 29 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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


.. 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')
4 changes: 1 addition & 3 deletions paddle/_coupons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion paddle/_licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def generate_license(
expires_at: DatetimeType = None,
) -> dict:
"""
`Generate License Paddle docs <https://developer.paddle.com/api-reference/product-api/licenses/createlicense>`_
""" # NOQA: E501
url = urljoin(self.vendors_v2, 'product/generate_license')
Expand Down
17 changes: 11 additions & 6 deletions paddle/_pay_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion paddle/_subscription_payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
Loading

0 comments on commit 4a5e07d

Please sign in to comment.