From 18079fce91e52df2c4fd609f03588cb96394d1c2 Mon Sep 17 00:00:00 2001 From: Marco Antonio Gil Date: Tue, 7 Jul 2026 11:43:43 +0200 Subject: [PATCH] PTHMINT-131: Add error handling for transport exceptions and server errors in Client --- src/multisafepay/client/client.py | 4 +- .../unit/client/test_unit_client.py | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/multisafepay/client/client.py b/src/multisafepay/client/client.py index 5854e30..a7330e4 100644 --- a/src/multisafepay/client/client.py +++ b/src/multisafepay/client/client.py @@ -344,6 +344,7 @@ def _create_request( "Content-Type": "application/json", } + response = None try: response = self.transport.request( method=method, @@ -354,7 +355,8 @@ def _create_request( response.raise_for_status() except Exception as e: if ( - hasattr(response, "status_code") + response is not None + and hasattr(response, "status_code") and 500 <= response.status_code < 600 ): raise ApiException(f"Request failed: {e}") from e diff --git a/tests/multisafepay/unit/client/test_unit_client.py b/tests/multisafepay/unit/client/test_unit_client.py index 8f6049d..395f8de 100644 --- a/tests/multisafepay/unit/client/test_unit_client.py +++ b/tests/multisafepay/unit/client/test_unit_client.py @@ -14,6 +14,7 @@ from multisafepay.client.credential_resolver import ( ScopedCredentialResolver, ) +from multisafepay.exception.api import ApiException from multisafepay.transport import RequestsTransport requests = pytest.importorskip("requests") @@ -54,6 +55,32 @@ def request(self: "_CaptureTransport", **kwargs: dict) -> _FakeResponse: return _FakeResponse() +class _FailingTransport: + """Transport stub that fails before returning a response.""" + + @staticmethod + def request(**kwargs: dict) -> _FakeResponse: + raise RuntimeError("network unavailable") + + +class _ServerErrorResponse(_FakeResponse): + """Response stub that raises for a server error.""" + + status_code = 500 + + @staticmethod + def raise_for_status() -> None: + raise RuntimeError("server error") + + +class _ServerErrorTransport: + """Transport stub that returns a server error response.""" + + @staticmethod + def request(**kwargs: dict) -> _ServerErrorResponse: + return _ServerErrorResponse() + + def _build_resolver_client( resolver: ScopedCredentialResolver, transport: _CaptureTransport, @@ -328,6 +355,30 @@ def test_create_delete_request_sends_authorization_header() -> None: assert transport.headers["Authorization"] == "Bearer test_key" +def test_request_exception_without_response_is_reraised() -> None: + """Reraise transport exceptions when no response exists.""" + client = Client( + api_key="test_key", + is_production=False, + transport=_FailingTransport(), + ) + + with pytest.raises(RuntimeError, match="network unavailable"): + client.create_get_request("json/orders") + + +def test_server_error_response_raises_api_exception() -> None: + """Wrap server error responses in ApiException.""" + client = Client( + api_key="test_key", + is_production=False, + transport=_ServerErrorTransport(), + ) + + with pytest.raises(ApiException, match="Request failed: server error"): + client.create_get_request("json/orders") + + def test_resolve_api_key_uses_credential_resolver() -> None: """Prefer credential resolver when both api_key and resolver exist.""" transport = _CaptureTransport()