diff --git a/pyproject.toml b/pyproject.toml index d337167..a2a7f99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = [ "version" ] [tool.poetry] name = "enablebanking_sdk" -version = "0.1.6" +version = "0.1.7" description = "" authors = ["NOCFO "] license = "MIT" diff --git a/src/enablebanking_sdk/service/integration.py b/src/enablebanking_sdk/service/integration.py index dff2bec..2d659dc 100644 --- a/src/enablebanking_sdk/service/integration.py +++ b/src/enablebanking_sdk/service/integration.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta from typing import Tuple -from requests.exceptions import HTTPError +from requests.exceptions import HTTPError, JSONDecodeError import jwt import requests @@ -104,6 +104,17 @@ def _request( response=err.response, ) from err + except JSONDecodeError as err: + # A successful status code with a non-JSON body (e.g. an empty 200 + # response) still leaves callers with nothing usable. Surface it as an + # EnableBankingException so it is handled like any other request failure + # instead of leaking a raw JSONDecodeError. + raise EnableBankingException( + err, + request=response.request, + response=response, + ) from err + def get_aspsps(self, country: str, psu_type: str) -> dict: return self._request( method="GET", diff --git a/tests/test_enablebanking_service.py b/tests/test_enablebanking_service.py index 3482e4b..fd38af8 100644 --- a/tests/test_enablebanking_service.py +++ b/tests/test_enablebanking_service.py @@ -2,10 +2,13 @@ from datetime import date, datetime from unittest import mock +import requests + from src.enablebanking_sdk.constants import PSUType from src.enablebanking_sdk.constants.transaction_fetch_strategy import ( TransactionsFetchStrategy, ) +from src.enablebanking_sdk.exceptions import EnableBankingException from src.enablebanking_sdk.models import AspspData from src.enablebanking_sdk.service import EnableBankingService, EnableBankingIntegration from tests.utils import get_json_fixtures @@ -124,3 +127,38 @@ def test_get_account_details(self, request_mock): ) self.assertEqual(data.details, "My account nickname") + + @mock.patch.object( + EnableBankingIntegration, "_get_token", return_value="mock-token" + ) + @mock.patch("src.enablebanking_sdk.service.integration.requests.request") + def test_request_wraps_non_json_body_as_enablebanking_exception( + self, request_mock, _token_mock + ): + response = requests.Response() + response.status_code = 200 + response._content = b"" # empty body -> response.json() raises JSONDecodeError + request_mock.return_value = response + + with self.assertRaises(EnableBankingException) as ctx: + self.service.integration._request(method="GET", path="/accounts/x/balances") + + # The originating response is attached so callers can still inspect the status code. + self.assertEqual(ctx.exception.response.status_code, 200) + + @mock.patch.object( + EnableBankingIntegration, "_get_token", return_value="mock-token" + ) + @mock.patch("src.enablebanking_sdk.service.integration.requests.request") + def test_request_wraps_http_error_as_enablebanking_exception( + self, request_mock, _token_mock + ): + response = requests.Response() + response.status_code = 500 + response._content = b"Internal Server Error" + request_mock.return_value = response + + with self.assertRaises(EnableBankingException) as ctx: + self.service.integration._request(method="GET", path="/accounts/x/balances") + + self.assertEqual(ctx.exception.response.status_code, 500)