From f0c40d312777b975eee26c6a663297624fd5eb13 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 25 May 2022 18:46:12 +0200 Subject: [PATCH] utils/soap: add wrapping of zeep errors inside APIError (#58925) --- passerelle/apps/soap/models.py | 1 + passerelle/contrib/utils/axel.py | 2 +- passerelle/utils/soap.py | 53 ++++++++++++++++++++++++++++++++ tests/test_soap.py | 16 +++++----- tests/test_utils_soap.py | 30 +++++++++++++++++- 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/passerelle/apps/soap/models.py b/passerelle/apps/soap/models.py index 1d0e3164..e8d08fd7 100644 --- a/passerelle/apps/soap/models.py +++ b/passerelle/apps/soap/models.py @@ -79,6 +79,7 @@ class SOAPConnector(BaseResource, HTTPResource): settings=zeep.Settings( strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order ), + api_error=True, **kwargs, ) diff --git a/passerelle/contrib/utils/axel.py b/passerelle/contrib/utils/axel.py index 6f80930e..cd4e2e3e 100644 --- a/passerelle/contrib/utils/axel.py +++ b/passerelle/contrib/utils/axel.py @@ -225,7 +225,7 @@ class Operation: return schema def __call__(self, resource, request_data=None): - client = resource.soap_client() + client = resource.soap_client(api_error=True) serialized_request = '' if self.request_converter: diff --git a/passerelle/utils/soap.py b/passerelle/utils/soap.py index 338fe860..26334377 100644 --- a/passerelle/utils/soap.py +++ b/passerelle/utils/soap.py @@ -19,6 +19,9 @@ from urllib import parse as urlparse from requests import RequestException from zeep import Client from zeep.cache import InMemoryCache +from zeep.exceptions import Error as ZeepError +from zeep.exceptions import Fault, TransportError +from zeep.proxy import OperationProxy, ServiceProxy from zeep.transports import Transport from passerelle.utils.jsonresponse import APIError @@ -28,6 +31,48 @@ class SOAPError(APIError): pass +class SOAPServiceUnreachable(SOAPError): + def __init__(self, client, exception): + super().__init__( + f'SOAP service at {client.wsdl.location} is unreachable. Please contact its administrator', + data={ + 'wsdl': client.wsdl.location, + 'status_code': exception.status_code, + 'error': str(exception), + }, + ) + + +class SOAPFault(SOAPError): + def __init__(self, client, fault): + super().__init__( + f'SOAP service at {client.wsdl.location} returned an error "{fault.message or fault.code}"', + data={ + 'soap_fault': fault.__dict__, + }, + ) + + +class OperationProxyWrapper(OperationProxy): + def __call__(self, *args, **kwargs): + client = self._proxy._client + try: + return super().__call__(*args, **kwargs) + except TransportError as transport_error: + raise SOAPServiceUnreachable(client, transport_error) + except Fault as fault: + raise SOAPFault(client, fault) + except ZeepError as zeep_error: + raise SOAPError(str(zeep_error)) + + +class ServiceProxyWrapper(ServiceProxy): + def __getitem__(self, key): + operation = super().__getitem__(key) + operation.__class__ = OperationProxyWrapper + return operation + + class SOAPClient(Client): """Wrapper around zeep.Client @@ -36,6 +81,7 @@ class SOAPClient(Client): def __init__(self, resource, **kwargs): wsdl_url = kwargs.pop('wsdl_url', None) or resource.wsdl_url + self.api_error = kwargs.pop('api_error', False) transport_kwargs = kwargs.pop('transport_kwargs', {}) transport_class = getattr(resource, 'soap_transport_class', SOAPTransport) transport = transport_class( @@ -43,6 +89,13 @@ class SOAPClient(Client): ) super().__init__(wsdl_url, transport=transport, **kwargs) + @property + def service(self): + service = super().service + if self.api_error: + service.__class__ = ServiceProxyWrapper + return service + class ResponseFixContentWrapper: def __init__(self, response): diff --git a/tests/test_soap.py b/tests/test_soap.py index 5634b264..3a87ea08 100644 --- a/tests/test_soap.py +++ b/tests/test_soap.py @@ -139,6 +139,7 @@ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> 'greeting': 'Hello', 'who': 'John!', } + VALIDATION_ERROR = 'Missing element firstName (sayHello.firstName)' class SOAP12(SOAP11): @@ -254,6 +255,7 @@ class SOAP12(SOAP11): 'greeting': 'Hello', 'who': ['John!'], } + VALIDATION_ERROR = 'Expected at least 1 items (minOccurs check) 0 items found. (sayHello.firstName)' class BrokenSOAP12(SOAP12): @@ -301,13 +303,13 @@ def test_schemas(connector, soap): assert list(connector.operations_and_schemas) == [('sayHello', soap.INPUT_SCHEMA, soap.OUTPUT_SCHEMA)] -def test_say_hello_method_validation_error(connector, app): - resp = app.get('/soap/test/method/sayHello/', status=500) - assert dict(resp.json, err_desc=None) == { - 'err': 1, - 'err_class': 'zeep.exceptions.ValidationError', - 'err_desc': None, - 'data': None, +def test_say_hello_method_validation_error(connector, soap, app): + resp = app.get('/soap/test/method/sayHello/') + assert resp.json == { + "err": 1, + "err_class": "passerelle.utils.soap.SOAPError", + "err_desc": soap.VALIDATION_ERROR, + "data": None, } diff --git a/tests/test_utils_soap.py b/tests/test_utils_soap.py index 35fc9472..9e0d5f42 100644 --- a/tests/test_utils_soap.py +++ b/tests/test_utils_soap.py @@ -18,9 +18,10 @@ import pytest import requests from django.utils.encoding import force_bytes from zeep import Settings -from zeep.exceptions import TransportError, XMLParseError +from zeep.exceptions import Fault, TransportError, XMLParseError from zeep.plugins import Plugin +from passerelle.utils.jsonresponse import APIError from passerelle.utils.soap import SOAPClient WSDL = 'tests/data/soap.wsdl' @@ -107,3 +108,30 @@ def test_remove_first_bytes_for_xml(mocked_post): assert len(result) == 2 assert result['skipMe'] == 1.2 assert result['price'] == 4.2 + + +@mock.patch('requests.sessions.Session.send') +def test_api_error(mocked_send, caplog): + response = requests.Response() + response.status_code = 502 + response.headers = {'Content-Type': 'application/xml'} + response._content = b'x' + mocked_send.return_value = response + + soap_resource = SOAPResource() + client = SOAPClient(soap_resource) + with pytest.raises(TransportError): + client.service.GetLastTradePrice(tickerSymbol='banana') + + client = SOAPClient(soap_resource, api_error=True) + with pytest.raises(APIError, match=r'SOAP service at.*is unreachable'): + client.service.GetLastTradePrice(tickerSymbol='banana') + with mock.patch('zeep.proxy.OperationProxy.__call__') as operation_proxy_call: + + operation_proxy_call.side_effect = Fault('boom!') + with pytest.raises(APIError, match=r'returned an error.*"boom!"'): + client.service.GetLastTradePrice(tickerSymbol='banana') + + operation_proxy_call.side_effect = XMLParseError('Unexpected element') + with pytest.raises(APIError, match=r'Unexpected element'): + client.service.GetLastTradePrice(tickerSymbol='banana') -- 2.35.1