0001-add-a-generic-soap-connector-60836.patch
passerelle/apps/soap/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.24 on 2022-01-19 16:37 |
|
2 | ||
3 |
from django.db import migrations, models |
|
4 | ||
5 | ||
6 |
class Migration(migrations.Migration): |
|
7 | ||
8 |
initial = True |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('base', '0029_auto_20210202_1627'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.CreateModel( |
|
16 |
name='SOAPConnector', |
|
17 |
fields=[ |
|
18 |
( |
|
19 |
'id', |
|
20 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
21 |
), |
|
22 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
23 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
24 |
('description', models.TextField(verbose_name='Description')), |
|
25 |
( |
|
26 |
'basic_auth_username', |
|
27 |
models.CharField( |
|
28 |
blank=True, max_length=128, verbose_name='Basic authentication username' |
|
29 |
), |
|
30 |
), |
|
31 |
( |
|
32 |
'basic_auth_password', |
|
33 |
models.CharField( |
|
34 |
blank=True, max_length=128, verbose_name='Basic authentication password' |
|
35 |
), |
|
36 |
), |
|
37 |
( |
|
38 |
'client_certificate', |
|
39 |
models.FileField( |
|
40 |
blank=True, null=True, upload_to='', verbose_name='TLS client certificate' |
|
41 |
), |
|
42 |
), |
|
43 |
( |
|
44 |
'trusted_certificate_authorities', |
|
45 |
models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'), |
|
46 |
), |
|
47 |
( |
|
48 |
'verify_cert', |
|
49 |
models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'), |
|
50 |
), |
|
51 |
( |
|
52 |
'http_proxy', |
|
53 |
models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'), |
|
54 |
), |
|
55 |
( |
|
56 |
'wsdl_url', |
|
57 |
models.URLField( |
|
58 |
help_text='URL of the WSDL file', max_length=400, verbose_name='WSDL URL' |
|
59 |
), |
|
60 |
), |
|
61 |
( |
|
62 |
'zeep_strict', |
|
63 |
models.BooleanField(default=True, verbose_name='Be strict with returned XML'), |
|
64 |
), |
|
65 |
( |
|
66 |
'zeep_xsd_ignore_sequence_order', |
|
67 |
models.BooleanField(default=False, verbose_name='Ignore sequence order'), |
|
68 |
), |
|
69 |
( |
|
70 |
'users', |
|
71 |
models.ManyToManyField( |
|
72 |
blank=True, |
|
73 |
related_name='_soapconnector_users_+', |
|
74 |
related_query_name='+', |
|
75 |
to='base.ApiUser', |
|
76 |
), |
|
77 |
), |
|
78 |
], |
|
79 |
options={ |
|
80 |
'verbose_name': 'SOAP connector', |
|
81 |
}, |
|
82 |
), |
|
83 |
] |
passerelle/apps/soap/models.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import collections |
|
18 | ||
19 |
import zeep |
|
20 |
import zeep.exceptions |
|
21 |
import zeep.helpers |
|
22 |
import zeep.xsd |
|
23 |
from django.db import models |
|
24 |
from django.utils.functional import cached_property |
|
25 |
from django.utils.translation import ugettext_lazy as _ |
|
26 | ||
27 |
from passerelle.base.models import BaseResource, HTTPResource |
|
28 |
from passerelle.utils.api import endpoint |
|
29 |
from passerelle.utils.json import unflatten |
|
30 | ||
31 | ||
32 |
class SOAPConnector(BaseResource, HTTPResource): |
|
33 |
wsdl_url = models.URLField( |
|
34 |
max_length=400, verbose_name=_('WSDL URL'), help_text=_('URL of the WSDL file') |
|
35 |
) |
|
36 |
zeep_strict = models.BooleanField(default=False, verbose_name=_('Be strict with returned XML')) |
|
37 |
zeep_xsd_ignore_sequence_order = models.BooleanField( |
|
38 |
default=True, verbose_name=_('Ignore sequence order') |
|
39 |
) |
|
40 |
category = _('Business Process Connectors') |
|
41 | ||
42 |
class Meta: |
|
43 |
verbose_name = _('SOAP connector') |
|
44 | ||
45 |
@classmethod |
|
46 |
def get_manager_form_class(cls, **kwargs): |
|
47 |
form_class = super().get_manager_form_class(**kwargs) |
|
48 |
fields = list(form_class.base_fields.items()) |
|
49 |
form_class.base_fields = collections.OrderedDict(fields[:2] + fields[-3:] + fields[2:-3]) |
|
50 |
return form_class |
|
51 | ||
52 |
@cached_property |
|
53 |
def client(self): |
|
54 |
return self.soap_client( |
|
55 |
wsdl_url=self.wsdl_url, |
|
56 |
settings=zeep.Settings( |
|
57 |
strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order |
|
58 |
), |
|
59 |
) |
|
60 | ||
61 |
@endpoint( |
|
62 |
methods=['post'], |
|
63 |
perm='can_access', |
|
64 |
name='method', |
|
65 |
pattern=r'^(?P<method_name>\w+)/$', |
|
66 |
example_pattern='method_name/', |
|
67 |
description_get=_('Call a SOAP method'), |
|
68 |
description_post=_('Call a SOAP method'), |
|
69 |
post_json_schema={'type': 'object'}, |
|
70 |
) |
|
71 |
def method(self, request, method_name, post_data=None, **kwargs): |
|
72 |
def jsonify(data): |
|
73 |
if isinstance(data, (dict, collections.OrderedDict)): |
|
74 |
# ignore _raw_elements, zeep put there nodes not maching the |
|
75 |
# XSD when strict parsing is disabled. |
|
76 |
return { |
|
77 |
jsonify(k): jsonify(v) |
|
78 |
for k, v in data.items() |
|
79 |
if (self.zeep_strict or k != '_raw_elements') |
|
80 |
} |
|
81 |
elif isinstance(data, (list, tuple, collections.deque)): |
|
82 |
return [jsonify(item) for item in data] |
|
83 |
else: |
|
84 |
return data |
|
85 | ||
86 |
payload = {} |
|
87 |
for k in request.GET: |
|
88 |
if k == 'raise': |
|
89 |
continue |
|
90 |
value = request.GET.getlist(k) |
|
91 |
if len(value) > 1: |
|
92 |
payload[k] = value |
|
93 |
else: |
|
94 |
payload[k] = value[0] |
|
95 |
payload.update(post_data or {}) |
|
96 |
payload = unflatten(payload) |
|
97 |
try: |
|
98 |
soap_response = getattr(self.client.service, method_name)(**payload) |
|
99 |
except zeep.exceptions.ValidationError as e: |
|
100 |
e.status_code = 400 |
|
101 |
raise e |
|
102 |
serialized = zeep.helpers.serialize_object(soap_response) |
|
103 |
json_response = jsonify(serialized) |
|
104 |
return {'err': 0, 'data': json_response} |
|
105 | ||
106 |
method.endpoint_info.methods.append('get') |
|
107 | ||
108 |
def get_endpoints_infos(self): |
|
109 |
endpoints = super().get_endpoints_infos() |
|
110 |
for name, input_schema, output_schema in self.operations_and_schemas: |
|
111 |
kwargs = dict( |
|
112 |
name='method', |
|
113 |
pattern=f'{name}/', |
|
114 |
example_pattern=f'{name}/', |
|
115 |
description=f'Method {name}', |
|
116 |
json_schema_response={ |
|
117 |
'type': 'object', |
|
118 |
'properties': collections.OrderedDict( |
|
119 |
[ |
|
120 |
('err', {'type': 'integer'}), |
|
121 |
('data', output_schema), |
|
122 |
] |
|
123 |
), |
|
124 |
}, |
|
125 |
) |
|
126 |
if input_schema: |
|
127 |
kwargs['post_json_schema'] = input_schema |
|
128 |
endpoints.append(endpoint(**kwargs)) |
|
129 |
endpoints[-1].object = self |
|
130 |
endpoints[-1].func = lambda request: None |
|
131 |
if input_schema: |
|
132 |
endpoints[-1].http_method = 'post' |
|
133 |
else: |
|
134 |
endpoints[-1].http_method = 'get' |
|
135 |
return endpoints |
|
136 | ||
137 |
@property |
|
138 |
def operations_and_schemas(self): |
|
139 |
operations = self.client.service._binding._operations |
|
140 |
for name in operations: |
|
141 |
operation = operations[name] |
|
142 |
input_type = operation.input.body.type |
|
143 |
output_type = operation.output.body.type |
|
144 |
input_schema = self.type2schema(input_type, keep_root=True) |
|
145 |
output_schema = self.type2schema(output_type, compress=True) |
|
146 |
yield name, input_schema, output_schema |
|
147 | ||
148 |
def type2schema(self, xsd_type, keep_root=False, compress=False): |
|
149 |
# simplify schema: when a type contains a unique element, it will try |
|
150 |
# to match any dict or list with it on input and will flatten the |
|
151 |
# schema on output. |
|
152 |
if ( |
|
153 |
isinstance(xsd_type, zeep.xsd.ComplexType) |
|
154 |
and len(xsd_type.elements) == 1 |
|
155 |
and not keep_root |
|
156 |
and compress |
|
157 |
): |
|
158 |
if xsd_type.elements[0][1].max_occurs != 1: |
|
159 |
return { |
|
160 |
'type': 'array', |
|
161 |
'items': self.type2schema(xsd_type.elements[0][1].type, compress=compress), |
|
162 |
} |
|
163 |
return self.type2schema(xsd_type.elements[0][1].type, compress=compress) |
|
164 |
if isinstance(xsd_type, zeep.xsd.ComplexType): |
|
165 |
properties = collections.OrderedDict() |
|
166 |
schema = { |
|
167 |
'type': 'object', |
|
168 |
'properties': properties, |
|
169 |
} |
|
170 |
for key, element in xsd_type.elements: |
|
171 |
if element.min_occurs > 0: |
|
172 |
schema.setdefault('required', []).append(key) |
|
173 |
element_schema = self.type2schema(element.type, compress=compress) |
|
174 |
if element.max_occurs == 'unbounded' or element.max_occurs > 1: |
|
175 |
element_schema = {'type': 'array', 'items': element_schema} |
|
176 |
properties[key] = element_schema |
|
177 |
if not properties: |
|
178 |
return None |
|
179 |
return schema |
|
180 |
if isinstance(xsd_type, zeep.xsd.BuiltinType): |
|
181 |
return {'type': 'string'} |
|
182 |
return f'!!! UNKNOWN TYPE {xsd_type} !!!' |
passerelle/apps/soap/templates/soap/soapconnector_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
passerelle/settings.py | ||
---|---|---|
162 | 162 |
'passerelle.apps.plone_restapi', |
163 | 163 |
'passerelle.apps.sector', |
164 | 164 |
'passerelle.apps.sfr_dmc', |
165 |
'passerelle.apps.soap', |
|
165 | 166 |
'passerelle.apps.solis', |
166 | 167 |
'passerelle.apps.sp_fr', |
167 | 168 |
'passerelle.apps.twilio', |
tests/test_soap.py | ||
---|---|---|
13 | 13 |
# You should have received a copy of the GNU Affero General Public License |
14 | 14 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | |
16 |
import mock |
|
16 |
import urllib.parse |
|
17 | ||
17 | 18 |
import pytest |
18 |
import requests |
|
19 |
from django.utils.encoding import force_bytes |
|
20 |
from zeep import Settings |
|
21 |
from zeep.exceptions import TransportError, XMLParseError |
|
22 |
from zeep.plugins import Plugin |
|
19 |
import utils |
|
23 | 20 | |
24 |
from passerelle.utils.soap import SOAPClient
|
|
21 |
from passerelle.apps.soap.models import SOAPConnector
|
|
25 | 22 | |
26 |
WSDL = 'tests/data/soap.wsdl' |
|
27 | 23 | |
24 |
class SOAP11: |
|
25 |
VERSION = '1.1' |
|
26 |
ENDPOINT_URL = 'https://www.examples.com/SayHello/' |
|
27 |
WSDL_CONTENT = '''\ |
|
28 |
<definitions name = "HelloService" |
|
29 |
targetNamespace = "http://www.examples.com/wsdl/HelloService.wsdl" |
|
30 |
xmlns = "http://schemas.xmlsoap.org/wsdl/" |
|
31 |
xmlns:soap = "http://schemas.xmlsoap.org/wsdl/soap/" |
|
32 |
xmlns:tns = "http://www.examples.com/wsdl/HelloService.wsdl" |
|
33 |
xmlns:xsd = "http://www.w3.org/2001/XMLSchema"> |
|
28 | 34 | |
29 |
class FooPlugin(Plugin): |
|
30 |
pass |
|
35 |
<types> |
|
36 |
<schema targetNamespace="http://www.examples.com/wsdl/HelloService.wsdl" xmlns="http://www.w3.org/2001/XMLSchema"> |
|
37 |
<element name="firstName"> |
|
38 |
<complexType name="listofstring"> |
|
39 |
<sequence> |
|
40 |
<element name="string" type="string" maxOccurs="unbounded"/> |
|
41 |
</sequence> |
|
42 |
</complexType> |
|
43 |
</element> |
|
44 |
</schema> |
|
45 |
</types> |
|
31 | 46 | |
47 |
<message name = "SayHelloRequest"> |
|
48 |
<part name = "firstName" element="tns:firstName"/> |
|
49 |
<part name = "lastName" type = "xsd:string"/> |
|
50 |
</message> |
|
32 | 51 | |
33 |
class BarPlugin(Plugin): |
|
34 |
pass |
|
52 |
<message name = "SayHelloResponse"> |
|
53 |
<part name = "greeting" type = "xsd:string"/> |
|
54 |
</message> |
|
35 | 55 | |
56 |
<portType name = "Hello_PortType"> |
|
57 |
<operation name = "sayHello"> |
|
58 |
<input message = "tns:SayHelloRequest"/> |
|
59 |
<output message = "tns:SayHelloResponse"/> |
|
60 |
</operation> |
|
61 |
</portType> |
|
36 | 62 | |
37 |
class SOAPResource(object): |
|
38 |
def __init__(self): |
|
39 |
self.requests = requests.Session() |
|
40 |
self.wsdl_url = WSDL |
|
63 |
<binding name = "Hello_Binding" type = "tns:Hello_PortType"> |
|
64 |
<soap:binding style = "rpc" |
|
65 |
transport = "http://schemas.xmlsoap.org/soap/http"/> |
|
66 |
<operation name = "sayHello"> |
|
67 |
<soap:operation soapAction = "sayHello"/> |
|
68 |
<input> |
|
69 |
<soap:body |
|
70 |
encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" |
|
71 |
namespace = "urn:examples:helloservice" |
|
72 |
use = "encoded"/> |
|
73 |
</input> |
|
41 | 74 | |
75 |
<output> |
|
76 |
<soap:body |
|
77 |
encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" |
|
78 |
namespace = "urn:examples:helloservice" |
|
79 |
use = "encoded"/> |
|
80 |
</output> |
|
81 |
</operation> |
|
82 |
</binding> |
|
42 | 83 | |
43 |
def test_soap_client(): |
|
44 |
soap_resource = SOAPResource() |
|
45 |
plugins = [FooPlugin, BarPlugin] |
|
46 |
client = SOAPClient(soap_resource, plugins=plugins) |
|
47 |
assert client.wsdl.location.endswith(WSDL) |
|
48 |
assert client.transport.session == soap_resource.requests |
|
49 |
assert client.transport.cache |
|
50 |
assert client.plugins == plugins |
|
84 |
<service name = "Hello_Service"> |
|
85 |
<documentation>WSDL File for HelloService</documentation> |
|
86 |
<port binding = "tns:Hello_Binding" name = "Hello_Port"> |
|
87 |
<soap:address |
|
88 |
location = "http://www.examples.com/SayHello/" /> |
|
89 |
</port> |
|
90 |
</service> |
|
91 |
</definitions>''' |
|
92 |
WSDL_URL = 'https://example.com/service.wsdl' |
|
93 |
SOAP_RESPONSE = '''\ |
|
94 |
<?xml version="1.0" encoding="utf-8"?> |
|
95 |
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|
96 |
xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
|
97 |
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> |
|
98 |
<soap:Body> |
|
99 |
<SayHelloResponse xmlns="urn:examples:helloservice"> |
|
100 |
<greeting>Hello John!</greeting> |
|
101 |
</SayHelloResponse> |
|
102 |
</soap:Body> |
|
103 |
</soap:Envelope>''' |
|
104 |
INPUT_SCHEMA = { |
|
105 |
'properties': { |
|
106 |
'firstName': { |
|
107 |
'properties': { |
|
108 |
'string': {'items': {'type': 'string'}, 'type': 'array'}, |
|
109 |
}, |
|
110 |
'required': ['string'], |
|
111 |
'type': 'object', |
|
112 |
}, |
|
113 |
'lastName': {'type': 'string'}, |
|
114 |
}, |
|
115 |
'required': ['firstName', 'lastName'], |
|
116 |
'type': 'object', |
|
117 |
} |
|
118 |
OUTPUT_SCHEMA = {'type': 'string'} |
|
119 |
INPUT_DATA = { |
|
120 |
'firstName/string/0': 'John', |
|
121 |
'firstName/string/1': 'Bill', |
|
122 |
'lastName': 'Doe', |
|
123 |
} |
|
51 | 124 | |
52 | 125 | |
53 |
@mock.patch('requests.sessions.Session.post') |
|
54 |
def test_disable_strict_mode(mocked_post): |
|
55 |
response = requests.Response() |
|
56 |
response.status_code = 200 |
|
57 |
response._content = force_bytes( |
|
58 |
'''<?xml version='1.0' encoding='utf-8'?> |
|
59 |
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"> |
|
60 |
<soap-env:Body> |
|
61 |
<ns0:TradePrice xmlns:ns0="http://example.com/stockquote.xsd"> |
|
62 |
<price>4.20</price> |
|
63 |
</ns0:TradePrice> |
|
64 |
</soap-env:Body> |
|
65 |
</soap-env:Envelope>''' |
|
66 |
) |
|
67 |
mocked_post.return_value = response |
|
68 | ||
69 |
soap_resource = SOAPResource() |
|
70 |
client = SOAPClient(soap_resource) |
|
71 |
match = "Unexpected element %s, expected %s" % (repr(u'price'), repr(u'skipMe')) |
|
72 |
with pytest.raises(XMLParseError, match=match): |
|
73 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
74 | ||
75 |
client = SOAPClient(soap_resource, settings=Settings(strict=False)) |
|
76 |
result = client.service.GetLastTradePrice(tickerSymbol='banana') |
|
77 |
assert len(result) == 2 |
|
78 |
assert result['skipMe'] is None |
|
79 |
assert result['price'] == 4.2 |
|
80 | ||
81 | ||
82 |
@mock.patch('requests.sessions.Session.post') |
|
83 |
def test_remove_first_bytes_for_xml(mocked_post): |
|
84 |
response = requests.Response() |
|
85 |
response.status_code = 200 |
|
86 |
response._content = force_bytes( |
|
87 |
'''blabla \n<?xml version='1.0' encoding='utf-8'?> |
|
88 |
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"> |
|
89 |
<soap-env:Body> |
|
90 |
<ns0:TradePrice xmlns:ns0="http://example.com/stockquote.xsd"> |
|
91 |
<skipMe>1.2</skipMe> |
|
92 |
<price>4.20</price> |
|
93 |
</ns0:TradePrice> |
|
94 |
</soap-env:Body> |
|
95 |
</soap-env:Envelope>\n bloublou''' |
|
126 |
class SOAP12(SOAP11): |
|
127 |
VERSION = '1.2' |
|
128 |
ENDPOINT_URL = 'https://www.examples.com/SayHello/' |
|
129 |
WSDL_CONTENT = f'''\ |
|
130 |
<?xml version="1.0"?> |
|
131 |
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" |
|
132 |
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" |
|
133 |
xmlns:tns="urn:examples:helloservice" |
|
134 |
targetNamespace="urn:examples:helloservice"> |
|
135 | ||
136 |
<wsdl:types> |
|
137 |
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
|
138 |
xmlns:tns="urn:examples:helloservice" |
|
139 |
targetNamespace="urn:examples:helloservice"> |
|
140 |
<xsd:element name="sayHello"> |
|
141 |
<xsd:complexType> |
|
142 |
<xsd:sequence> |
|
143 |
<xsd:element name="firstName" type="xsd:string" maxOccurs="unbounded"/> |
|
144 |
<xsd:element name="lastName" type="xsd:string"/> |
|
145 |
</xsd:sequence> |
|
146 |
</xsd:complexType> |
|
147 |
</xsd:element> |
|
148 |
<xsd:element name="sayHelloResponse"> |
|
149 |
<xsd:complexType> |
|
150 |
<xsd:sequence> |
|
151 |
<xsd:element name="greeting" type="xsd:string"/> |
|
152 |
</xsd:sequence> |
|
153 |
</xsd:complexType> |
|
154 |
</xsd:element> |
|
155 |
</xsd:schema> |
|
156 |
</wsdl:types> |
|
157 | ||
158 |
<wsdl:message name="sayHello"> |
|
159 |
<wsdl:part name="sayHelloInputPart" element="tns:sayHello"/> |
|
160 |
</wsdl:message> |
|
161 |
<wsdl:message name="sayHelloResponse"> |
|
162 |
<wsdl:part name="sayHelloOutputPart" element="tns:sayHelloResponse"/> |
|
163 |
</wsdl:message> |
|
164 | ||
165 |
<wsdl:portType name="sayHelloPortType"> |
|
166 |
<wsdl:operation name="sayHello"> |
|
167 |
<wsdl:input name="sayHello" message="tns:sayHello"/> |
|
168 |
<wsdl:output name="sayHelloResponse" message="tns:sayHelloResponse"/> |
|
169 |
</wsdl:operation> |
|
170 |
</wsdl:portType> |
|
171 | ||
172 |
<wsdl:binding name="sayHelloBinding" |
|
173 |
type="tns:sayHelloPortType"> |
|
174 |
<soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> |
|
175 |
<wsdl:operation name="sayHello"> |
|
176 |
<soap12:operation style="document"/> |
|
177 |
<wsdl:input name="sayHello"> |
|
178 |
<soap12:body use="literal"/> |
|
179 |
</wsdl:input> |
|
180 |
<wsdl:output name="sayHelloResponse"> |
|
181 |
<soap12:body use="literal"/> |
|
182 |
</wsdl:output> |
|
183 |
</wsdl:operation> |
|
184 |
</wsdl:binding> |
|
185 | ||
186 |
<wsdl:service name="sayHelloService"> |
|
187 |
<wsdl:port name="sayHelloPort" |
|
188 |
binding="tns:sayHelloBinding"> |
|
189 |
<soap12:address location="{ENDPOINT_URL}"/> |
|
190 |
</wsdl:port> |
|
191 |
</wsdl:service> |
|
192 |
</wsdl:definitions>''' |
|
193 |
SOAP_RESPONSE = '''\ |
|
194 |
<?xml version="1.0" encoding="UTF-8"?> |
|
195 |
<soap:Envelope |
|
196 |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|
197 |
xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
|
198 |
xmlns:soap="http://www.w3.org/2003/05/soap-envelope" |
|
199 |
xmlns:ser="http://www.herongyang.com/Service/"> |
|
200 |
<soap:Header/> |
|
201 |
<soap:Body> |
|
202 |
<sayHelloResponse xmlns="urn:examples:helloservice"> |
|
203 |
<greeting>Hello John!</greeting> |
|
204 |
</sayHelloResponse> |
|
205 |
</soap:Body> |
|
206 |
</soap:Envelope>''' |
|
207 |
INPUT_SCHEMA = { |
|
208 |
'type': 'object', |
|
209 |
'properties': { |
|
210 |
'firstName': {'type': 'array', 'items': {'type': 'string'}}, |
|
211 |
'lastName': {'type': 'string'}, |
|
212 |
}, |
|
213 |
'required': ['firstName', 'lastName'], |
|
214 |
} |
|
215 |
INPUT_DATA = { |
|
216 |
'firstName/0': 'John', |
|
217 |
'firstName/1': 'Bill', |
|
218 |
'lastName': 'Doe', |
|
219 |
} |
|
220 | ||
221 | ||
222 |
@pytest.fixture(params=[SOAP11, SOAP12]) |
|
223 |
def soap(request): |
|
224 |
p = request.param() |
|
225 |
with utils.mock_url(p.WSDL_URL, response=p.WSDL_CONTENT): |
|
226 |
with utils.mock_url(p.ENDPOINT_URL, response=p.SOAP_RESPONSE) as mock: |
|
227 |
p.endpoint_mock = mock |
|
228 |
yield p |
|
229 | ||
230 | ||
231 |
@pytest.fixture |
|
232 |
def connector(db, soap): |
|
233 |
return utils.setup_access_rights( |
|
234 |
SOAPConnector.objects.create( |
|
235 |
slug='test', wsdl_url=soap.WSDL_URL, zeep_strict=True, zeep_xsd_ignore_sequence_order=False |
|
236 |
) |
|
96 | 237 |
) |
97 |
mocked_post.return_value = response |
|
98 | 238 | |
99 |
soap_resource = SOAPResource() |
|
100 | 239 | |
101 |
client = SOAPClient(soap_resource) |
|
102 |
with pytest.raises(TransportError): |
|
103 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
240 |
def test_schemas(connector, soap): |
|
241 |
assert list(connector.operations_and_schemas) == [('sayHello', soap.INPUT_SCHEMA, soap.OUTPUT_SCHEMA)] |
|
242 | ||
243 | ||
244 |
def test_say_hello_method_validation_error(connector, app): |
|
245 |
resp = app.get('/soap/test/method/sayHello/', status=500) |
|
246 |
assert dict(resp.json, err_desc=None) == { |
|
247 |
'err': 1, |
|
248 |
'err_class': 'zeep.exceptions.ValidationError', |
|
249 |
'err_desc': None, |
|
250 |
'data': None, |
|
251 |
} |
|
252 | ||
253 | ||
254 |
def test_say_hello_method_ok_get(connector, app, caplog, soap): |
|
255 |
resp = app.get('/soap/test/method/sayHello/?' + urllib.parse.urlencode(soap.INPUT_DATA)) |
|
256 |
assert '>John<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
257 |
assert '>Bill<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
258 | ||
259 |
assert '>Doe<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
260 |
assert resp.json == {'data': 'Hello John!', 'err': 0} |
|
261 | ||
104 | 262 | |
105 |
client = SOAPClient(soap_resource, transport_kwargs={'remove_first_bytes_for_xml': True}) |
|
106 |
result = client.service.GetLastTradePrice(tickerSymbol='banana') |
|
107 |
assert len(result) == 2 |
|
108 |
assert result['skipMe'] == 1.2 |
|
109 |
assert result['price'] == 4.2 |
|
263 |
def test_say_hello_method_ok_post_json(connector, app, caplog, soap): |
|
264 |
resp = app.post_json('/soap/test/method/sayHello/', params=soap.INPUT_DATA) |
|
265 |
assert '>John<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
266 |
assert '>Bill<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
267 |
assert '>Doe<' in soap.endpoint_mock.handlers[0].call['requests'][-1].body.decode() |
|
268 |
assert resp.json == {'data': 'Hello John!', 'err': 0} |
tests/test_utils_soap.py | ||
---|---|---|
1 |
# Copyright (C) 2021 Entr'ouvert |
|
2 |
# |
|
3 |
# This program is free software: you can redistribute it and/or modify it |
|
4 |
# under the terms of the GNU Affero General Public License as published |
|
5 |
# by the Free Software Foundation, either version 3 of the License, or |
|
6 |
# (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU Affero General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU Affero General Public License |
|
14 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
15 | ||
16 |
import mock |
|
17 |
import pytest |
|
18 |
import requests |
|
19 |
from django.utils.encoding import force_bytes |
|
20 |
from zeep import Settings |
|
21 |
from zeep.exceptions import TransportError, XMLParseError |
|
22 |
from zeep.plugins import Plugin |
|
23 | ||
24 |
from passerelle.utils.soap import SOAPClient |
|
25 | ||
26 |
WSDL = 'tests/data/soap.wsdl' |
|
27 | ||
28 | ||
29 |
class FooPlugin(Plugin): |
|
30 |
pass |
|
31 | ||
32 | ||
33 |
class BarPlugin(Plugin): |
|
34 |
pass |
|
35 | ||
36 | ||
37 |
class SOAPResource(object): |
|
38 |
def __init__(self): |
|
39 |
self.requests = requests.Session() |
|
40 |
self.wsdl_url = WSDL |
|
41 | ||
42 | ||
43 |
def test_soap_client(): |
|
44 |
soap_resource = SOAPResource() |
|
45 |
plugins = [FooPlugin, BarPlugin] |
|
46 |
client = SOAPClient(soap_resource, plugins=plugins) |
|
47 |
assert client.wsdl.location.endswith(WSDL) |
|
48 |
assert client.transport.session == soap_resource.requests |
|
49 |
assert client.transport.cache |
|
50 |
assert client.plugins == plugins |
|
51 | ||
52 | ||
53 |
@mock.patch('requests.sessions.Session.post') |
|
54 |
def test_disable_strict_mode(mocked_post): |
|
55 |
response = requests.Response() |
|
56 |
response.status_code = 200 |
|
57 |
response._content = force_bytes( |
|
58 |
'''<?xml version='1.0' encoding='utf-8'?> |
|
59 |
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"> |
|
60 |
<soap-env:Body> |
|
61 |
<ns0:TradePrice xmlns:ns0="http://example.com/stockquote.xsd"> |
|
62 |
<price>4.20</price> |
|
63 |
</ns0:TradePrice> |
|
64 |
</soap-env:Body> |
|
65 |
</soap-env:Envelope>''' |
|
66 |
) |
|
67 |
mocked_post.return_value = response |
|
68 | ||
69 |
soap_resource = SOAPResource() |
|
70 |
client = SOAPClient(soap_resource) |
|
71 |
match = "Unexpected element %s, expected %s" % (repr(u'price'), repr(u'skipMe')) |
|
72 |
with pytest.raises(XMLParseError, match=match): |
|
73 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
74 | ||
75 |
client = SOAPClient(soap_resource, settings=Settings(strict=False)) |
|
76 |
result = client.service.GetLastTradePrice(tickerSymbol='banana') |
|
77 |
assert len(result) == 2 |
|
78 |
assert result['skipMe'] is None |
|
79 |
assert result['price'] == 4.2 |
|
80 | ||
81 | ||
82 |
@mock.patch('requests.sessions.Session.post') |
|
83 |
def test_remove_first_bytes_for_xml(mocked_post): |
|
84 |
response = requests.Response() |
|
85 |
response.status_code = 200 |
|
86 |
response._content = force_bytes( |
|
87 |
'''blabla \n<?xml version='1.0' encoding='utf-8'?> |
|
88 |
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"> |
|
89 |
<soap-env:Body> |
|
90 |
<ns0:TradePrice xmlns:ns0="http://example.com/stockquote.xsd"> |
|
91 |
<skipMe>1.2</skipMe> |
|
92 |
<price>4.20</price> |
|
93 |
</ns0:TradePrice> |
|
94 |
</soap-env:Body> |
|
95 |
</soap-env:Envelope>\n bloublou''' |
|
96 |
) |
|
97 |
mocked_post.return_value = response |
|
98 | ||
99 |
soap_resource = SOAPResource() |
|
100 | ||
101 |
client = SOAPClient(soap_resource) |
|
102 |
with pytest.raises(TransportError): |
|
103 |
client.service.GetLastTradePrice(tickerSymbol='banana') |
|
104 | ||
105 |
client = SOAPClient(soap_resource, transport_kwargs={'remove_first_bytes_for_xml': True}) |
|
106 |
result = client.service.GetLastTradePrice(tickerSymbol='banana') |
|
107 |
assert len(result) == 2 |
|
108 |
assert result['skipMe'] == 1.2 |
|
109 |
assert result['price'] == 4.2 |
|
0 |
- |