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( settings=zeep.Settings(
strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order
), ),
api_error=True,
**kwargs, **kwargs,
) )

View File

@ -225,7 +225,7 @@ class Operation:
return schema return schema
def __call__(self, resource, request_data=None): def __call__(self, resource, request_data=None):
client = resource.soap_client() client = resource.soap_client(api_error=True)
serialized_request = '' serialized_request = ''
if self.request_converter: if self.request_converter:

View File

@ -19,6 +19,9 @@ from urllib import parse as urlparse
from requests import RequestException from requests import RequestException
from zeep import Client from zeep import Client
from zeep.cache import InMemoryCache 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 zeep.transports import Transport
from passerelle.utils.jsonresponse import APIError from passerelle.utils.jsonresponse import APIError
@ -28,6 +31,48 @@ class SOAPError(APIError):
pass 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): class SOAPClient(Client):
"""Wrapper around zeep.Client """Wrapper around zeep.Client
@ -36,6 +81,7 @@ class SOAPClient(Client):
def __init__(self, resource, **kwargs): def __init__(self, resource, **kwargs):
wsdl_url = kwargs.pop('wsdl_url', None) or resource.wsdl_url 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_kwargs = kwargs.pop('transport_kwargs', {})
transport_class = getattr(resource, 'soap_transport_class', SOAPTransport) transport_class = getattr(resource, 'soap_transport_class', SOAPTransport)
transport = transport_class( transport = transport_class(
@ -43,6 +89,13 @@ class SOAPClient(Client):
) )
super().__init__(wsdl_url, transport=transport, **kwargs) 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: class ResponseFixContentWrapper:
def __init__(self, response): def __init__(self, response):

View File

@ -139,6 +139,7 @@ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
'greeting': 'Hello', 'greeting': 'Hello',
'who': 'John!', 'who': 'John!',
} }
VALIDATION_ERROR = 'Missing element firstName (sayHello.firstName)'
class SOAP12(SOAP11): class SOAP12(SOAP11):
@ -254,6 +255,7 @@ class SOAP12(SOAP11):
'greeting': 'Hello', 'greeting': 'Hello',
'who': ['John!'], 'who': ['John!'],
} }
VALIDATION_ERROR = 'Expected at least 1 items (minOccurs check) 0 items found. (sayHello.firstName)'
class BrokenSOAP12(SOAP12): 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)] assert list(connector.operations_and_schemas) == [('sayHello', soap.INPUT_SCHEMA, soap.OUTPUT_SCHEMA)]
def test_say_hello_method_validation_error(connector, app): def test_say_hello_method_validation_error(connector, soap, app):
resp = app.get('/soap/test/method/sayHello/', status=500) resp = app.get('/soap/test/method/sayHello/')
assert dict(resp.json, err_desc=None) == { assert resp.json == {
'err': 1, "err": 1,
'err_class': 'zeep.exceptions.ValidationError', "err_class": "passerelle.utils.soap.SOAPError",
'err_desc': None, "err_desc": soap.VALIDATION_ERROR,
'data': None, "data": None,
} }

View File

@ -18,9 +18,10 @@ import pytest
import requests import requests
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from zeep import Settings from zeep import Settings
from zeep.exceptions import TransportError, XMLParseError from zeep.exceptions import Fault, TransportError, XMLParseError
from zeep.plugins import Plugin from zeep.plugins import Plugin
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.soap import SOAPClient from passerelle.utils.soap import SOAPClient
WSDL = 'tests/data/soap.wsdl' WSDL = 'tests/data/soap.wsdl'
@ -107,3 +108,30 @@ def test_remove_first_bytes_for_xml(mocked_post):
assert len(result) == 2 assert len(result) == 2
assert result['skipMe'] == 1.2 assert result['skipMe'] == 1.2
assert result['price'] == 4.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')