Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,15 @@ def oauth():
## Exception handling

All the API responses that do not fall in the 2** status codes will cause a `CheckoutApiException`. The exception encapsulates
the `http_metadata` and a dictionary of `error_details`, if available.
the `http_metadata`, `request_id`, `error_type`, and a list of `error_details`, if available.

```python
try:
checkout_api.customers.get("customer_id")
except CheckoutApiException as err:
http_status_code = err.http_metadata.status_code
request_id = err.request_id
error_type = err.error_type
error_details = err.error_details
```

Expand Down
20 changes: 14 additions & 6 deletions checkout_sdk/exception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import absolute_import

import logging

from checkout_sdk.authorization_type import AuthorizationType
from checkout_sdk.utils import map_to_http_metadata

Expand Down Expand Up @@ -33,21 +35,27 @@ def invalid_key(key_type: AuthorizationType):

class CheckoutApiException(CheckoutException):
http_metadata: dict
request_id: str
error_details: list
error_type: str

def __init__(self, response):
self.http_metadata = map_to_http_metadata(response)
self.request_id = None
self.error_details = None
self.error_type = None

if response.text:
try:
payload = response.json()
self.request_id = payload.get('request_id')
self.error_details = payload.get('error_codes')
self.error_type = payload.get('error_type')
except ValueError:
self.error_details = None
self.error_type = None
else:
self.error_details = None
self.error_type = None
except (ValueError, KeyError, TypeError) as e:
logging.error("Failed to parse response JSON payload: %s", e)

if not self.request_id:
self.request_id = response.headers.get('Cko-Request-Id')

super().__init__('The API response status code ({}) does not indicate success.'
.format(response.status_code))
2 changes: 1 addition & 1 deletion tests/checkout_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class VisaCard:
name: str = 'Checkout Test'
number: str = '4242424242424242'
expiry_month: int = 6
expiry_year: int = 2025
expiry_year: int = 2030
cvv: str = '100'


Expand Down
78 changes: 69 additions & 9 deletions tests/exception_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,75 +66,117 @@
def test_checkout_api_exception():
response = Mock()
response.status_code = 400
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"]}'
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"], "request_id": "req_123456"}'
response.json.return_value = {
"error_type": "request_invalid",
"error_codes": ["invalid_field"]
"error_codes": ["invalid_field"],
"request_id": "req_123456"
}
response.headers = {}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == 400
assert exception.error_type == "request_invalid"
assert exception.error_details == ["invalid_field"]
assert exception.request_id == "req_123456"


def test_checkout_api_exception_without_error_details():
response = Mock()
response.status_code = 500
response.text = '{"message": "Internal Server Error"}'
response.text = '{"message": "Internal Server Error", "request_id": "req_789012"}'
response.json.return_value = {
"message": "Internal Server Error"
"message": "Internal Server Error",
"request_id": "req_789012"
}
response.headers = {}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == 500
assert exception.error_type is None
assert exception.error_details is None
assert exception.request_id == "req_789012"


def test_checkout_api_exception_empty_response():
response = Mock()
response.status_code = 404
response.text = ''
response.json.return_value = {}
response.headers = {'Cko-Request-Id': 'header_req_345678'}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == 404
assert exception.error_type is None
assert exception.error_details is None
assert exception.request_id == "header_req_345678"


def test_checkout_api_exception_non_json_response():
response = Mock()
response.status_code = 502
response.text = 'Bad Gateway'
response.json.side_effect = ValueError("No JSON object could be decoded")
response.headers = {'Cko-Request-Id': 'header_req_502502'}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == 502
assert exception.error_type is None
assert exception.error_details is None
assert exception.request_id == "header_req_502502"


def test_checkout_api_exception_request_id_from_header_fallback():
response = Mock()
response.status_code = 400
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"]}'
response.json.return_value = {
"error_type": "request_invalid",
"error_codes": ["invalid_field"]
}
response.headers = {'Cko-Request-Id': '0120e756-6d00-453c-a398-ff1643f9a873'}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.request_id == "0120e756-6d00-453c-a398-ff1643f9a873"
assert exception.error_type == "request_invalid"
assert exception.error_details == ["invalid_field"]


def test_checkout_api_exception_no_request_id_anywhere():
response = Mock()
response.status_code = 400
response.text = '{"error_type": "request_invalid"}'
response.json.return_value = {"error_type": "request_invalid"}
response.headers = {} # Sin Cko-Request-Id

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.request_id is None
assert exception.error_type == "request_invalid"


@pytest.mark.parametrize("status_code", [400, 401, 403, 404, 500])
def test_checkout_api_exception_various_status_codes(status_code):
response = Mock()
response.status_code = status_code
response.text = ''
response.json.return_value = {}
response.headers = {'Cko-Request-Id': f'req_{status_code}'}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == status_code
assert exception.request_id == f'req_{status_code}'


def test_map_to_http_metadata():
Expand All @@ -150,28 +192,46 @@
def test_checkout_api_exception_message():
response = Mock()
response.status_code = 400
response.text = '{"error_type": "invalid_request", "error_codes": ["bad_request"]}'
response.text = '{"error_type": "invalid_request", "error_codes": ["bad_request"], "request_id": "msg_req_400"}'
response.json.return_value = {
"error_type": "invalid_request",
"error_codes": ["bad_request"]
"error_codes": ["bad_request"],
"request_id": "msg_req_400"
}
response.headers = {}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
expected_message = "The API response status code (400) does not indicate success."
assert str(exception) == expected_message
assert exception.request_id == "msg_req_400"


def test_checkout_api_exception_no_response_text():
response = Mock()
response.status_code = 400
response.text = None
response.json.return_value = {}
response.headers = {'Cko-Request-Id': 'no_text_req_id'}

with pytest.raises(CheckoutApiException) as exc_info:
raise CheckoutApiException(response)
exception = exc_info.value
assert exception.http_metadata.status_code == 400
assert exception.error_type is None
assert exception.error_details is None
assert exception.request_id == "no_text_req_id"


def test_checkout_api_exception_logs_on_json_parse_error(caplog):
response = Mock()
response.status_code = 502
response.text = 'Bad Gateway'
response.json.side_effect = ValueError("No JSON object could be decoded")
response.headers = {'Cko-Request-Id': 'header_req_logging'}

with caplog.at_level("ERROR"):
with pytest.raises(CheckoutApiException):
raise CheckoutApiException(response)

assert any("Failed to parse response JSON payload" in m for m in caplog.messages)
3 changes: 2 additions & 1 deletion tests/payments/request_apm_payments_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ def test_should_request_alipay_plus_payment(default_api):
except CheckoutApiException as err:
assert err.args[0] == 'The API response status code (422) does not indicate success.'
assert err.error_type == 'invalid_request'
assert err.error_details[0] == 'reference_invalid'
assert err.error_details is not None and 'reference_invalid' in err.error_details
assert err.request_id is not None


def test_should_make_przelewy24_payment(default_api):
Expand Down
Loading