utils/soap: add wrapping of zeep errors inside APIError (#58925)
This commit is contained in:
parent
32d647583f
commit
1ec9dbcb13
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue