0001-utils-soap-add-wrapping-of-zeep-errors-inside-APIErr.patch
passerelle/apps/soap/models.py | ||
---|---|---|
79 | 79 |
settings=zeep.Settings( |
80 | 80 |
strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order |
81 | 81 |
), |
82 |
api_error=True, |
|
82 | 83 |
**kwargs, |
83 | 84 |
) |
84 | 85 |
passerelle/contrib/utils/axel.py | ||
---|---|---|
225 | 225 |
return schema |
226 | 226 | |
227 | 227 |
def __call__(self, resource, request_data=None): |
228 |
client = resource.soap_client() |
|
228 |
client = resource.soap_client(api_error=True)
|
|
229 | 229 | |
230 | 230 |
serialized_request = '' |
231 | 231 |
if self.request_converter: |
passerelle/utils/soap.py | ||
---|---|---|
19 | 19 |
from requests import RequestException |
20 | 20 |
from zeep import Client |
21 | 21 |
from zeep.cache import InMemoryCache |
22 |
from zeep.exceptions import Error as ZeepError |
|
23 |
from zeep.exceptions import Fault, TransportError |
|
24 |
from zeep.proxy import OperationProxy, ServiceProxy |
|
22 | 25 |
from zeep.transports import Transport |
23 | 26 | |
24 | 27 |
from passerelle.utils.jsonresponse import APIError |
... | ... | |
28 | 31 |
pass |
29 | 32 | |
30 | 33 | |
34 |
class SOAPServiceUnreachable(SOAPError): |
|
35 |
def __init__(self, client, exception): |
|
36 |
super().__init__( |
|
37 |
f'SOAP service at {client.wsdl.location} is unreachable. Please contact its administrator', |
|
38 |
data={ |
|
39 |
'wsdl': client.wsdl.location, |
|
40 |
'status_code': exception.status_code, |
|
41 |
'error': str(exception), |
|
42 |
}, |
|
43 |
) |
|
44 | ||
45 | ||
46 |
class SOAPFault(SOAPError): |
|
47 |
def __init__(self, client, fault): |
|
48 |
super().__init__( |
|
49 |
f'SOAP service at {client.wsdl.location} returned an error "{fault.message or fault.code}"', |
|
50 |
data={ |
|
51 |
'soap_fault': fault.__dict__, |
|
52 |
}, |
|
53 |
) |
|
54 | ||
55 | ||
56 |
class OperationProxyWrapper(OperationProxy): |
|
57 |
def __call__(self, *args, **kwargs): |
|
58 |
client = self._proxy._client |
|
59 |
try: |
|
60 |
return super().__call__(*args, **kwargs) |
|
61 |
except TransportError as transport_error: |
|
62 |
raise SOAPServiceUnreachable(client, transport_error) |
|
63 |
except Fault as fault: |
|
64 |
raise SOAPFault(client, fault) |
|
65 |
except ZeepError as zeep_error: |
|
66 |
raise SOAPError(str(zeep_error)) |
|
67 | ||
68 | ||
69 |
class ServiceProxyWrapper(ServiceProxy): |
|
70 |
def __getitem__(self, key): |
|
71 |
operation = super().__getitem__(key) |
|
72 |
operation.__class__ = OperationProxyWrapper |
|
73 |
return operation |
|
74 | ||
75 | ||
31 | 76 |
class SOAPClient(Client): |
32 | 77 |
"""Wrapper around zeep.Client |
33 | 78 | |
... | ... | |
36 | 81 | |
37 | 82 |
def __init__(self, resource, **kwargs): |
38 | 83 |
wsdl_url = kwargs.pop('wsdl_url', None) or resource.wsdl_url |
84 |
self.api_error = kwargs.pop('api_error', False) |
|
39 | 85 |
transport_kwargs = kwargs.pop('transport_kwargs', {}) |
40 | 86 |
transport_class = getattr(resource, 'soap_transport_class', SOAPTransport) |
41 | 87 |
transport = transport_class( |
... | ... | |
43 | 89 |
) |
44 | 90 |
super().__init__(wsdl_url, transport=transport, **kwargs) |
45 | 91 | |
92 |
@property |
|
93 |
def service(self): |
|
94 |
service = super().service |
|
95 |
if self.api_error: |
|
96 |
service.__class__ = ServiceProxyWrapper |
|
97 |
return service |
|
98 | ||
46 | 99 | |
47 | 100 |
class ResponseFixContentWrapper: |
48 | 101 |
def __init__(self, response): |
tests/test_soap.py | ||
---|---|---|
139 | 139 |
'greeting': 'Hello', |
140 | 140 |
'who': 'John!', |
141 | 141 |
} |
142 |
VALIDATION_ERROR = 'Missing element firstName (sayHello.firstName)' |
|
142 | 143 | |
143 | 144 | |
144 | 145 |
class SOAP12(SOAP11): |
... | ... | |
254 | 255 |
'greeting': 'Hello', |
255 | 256 |
'who': ['John!'], |
256 | 257 |
} |
258 |
VALIDATION_ERROR = 'Expected at least 1 items (minOccurs check) 0 items found. (sayHello.firstName)' |
|
257 | 259 | |
258 | 260 | |
259 | 261 |
class BrokenSOAP12(SOAP12): |
... | ... | |
301 | 303 |
assert list(connector.operations_and_schemas) == [('sayHello', soap.INPUT_SCHEMA, soap.OUTPUT_SCHEMA)] |
302 | 304 | |
303 | 305 | |
304 |
def test_say_hello_method_validation_error(connector, app): |
|
305 |
resp = app.get('/soap/test/method/sayHello/', status=500)
|
|
306 |
assert dict(resp.json, err_desc=None) == {
|
|
307 |
'err': 1,
|
|
308 |
'err_class': 'zeep.exceptions.ValidationError',
|
|
309 |
'err_desc': None,
|
|
310 |
'data': None,
|
|
306 |
def test_say_hello_method_validation_error(connector, soap, app):
|
|
307 |
resp = app.get('/soap/test/method/sayHello/') |
|
308 |
assert resp.json == {
|
|
309 |
"err": 1,
|
|
310 |
"err_class": "passerelle.utils.soap.SOAPError",
|
|
311 |
"err_desc": soap.VALIDATION_ERROR,
|
|
312 |
"data": None,
|
|
311 | 313 |
} |
312 | 314 | |
313 | 315 |
tests/test_utils_soap.py | ||
---|---|---|
18 | 18 |
import requests |
19 | 19 |
from django.utils.encoding import force_bytes |
20 | 20 |
from zeep import Settings |
21 |
from zeep.exceptions import TransportError, XMLParseError |
|
21 |
from zeep.exceptions import Fault, TransportError, XMLParseError
|
|
22 | 22 |
from zeep.plugins import Plugin |
23 | 23 | |
24 |
from passerelle.utils.jsonresponse import APIError |
|
24 | 25 |
from passerelle.utils.soap import SOAPClient |
25 | 26 | |
26 | 27 |
WSDL = 'tests/data/soap.wsdl' |
... | ... | |
107 | 108 |
assert len(result) == 2 |
108 | 109 |
assert result['skipMe'] == 1.2 |
109 | 110 |
assert result['price'] == 4.2 |
111 | ||
112 | ||
113 |
@mock.patch('requests.sessions.Session.send') |
|
114 |
def test_api_error(mocked_send, caplog): |
|
115 |
response = requests.Response() |
|
116 |
response.status_code = 502 |
|
117 |
response.headers = {'Content-Type': 'application/xml'} |
|
118 |
response._content = b'x' |
|
119 |
mocked_send.return_value = response |
|
120 | ||
121 |
soap_resource = SOAPResource() |
|
122 |
client = SOAPClient(soap_resource) |
|
123 |
with pytest.raises(TransportError): |
|
124 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
125 | ||
126 |
client = SOAPClient(soap_resource, api_error=True) |
|
127 |
with pytest.raises(APIError, match=r'SOAP service at.*is unreachable'): |
|
128 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
129 |
with mock.patch('zeep.proxy.OperationProxy.__call__') as operation_proxy_call: |
|
130 | ||
131 |
operation_proxy_call.side_effect = Fault('boom!') |
|
132 |
with pytest.raises(APIError, match=r'returned an error.*"boom!"'): |
|
133 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
134 | ||
135 |
operation_proxy_call.side_effect = XMLParseError('Unexpected element') |
|
136 |
with pytest.raises(APIError, match=r'Unexpected element'): |
|
137 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
110 |
- |