utils/soap: add wrapping of zeep errors inside APIError (#58925)

This commit is contained in:
Benjamin Dauvergne 2022-05-25 18:46:12 +02:00
parent 32d647583f
commit 1ec9dbcb13
5 changed files with 93 additions and 9 deletions

View File

@ -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,
)

View File

@ -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:

View File

@ -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):

View File

@ -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,
}

View File

@ -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')