Projet

Général

Profil

0001-add-a-generic-soap-connector-60836.patch

Benjamin Dauvergne, 23 mars 2022 11:14

Télécharger (28,4 ko)

Voir les différences:

Subject: [PATCH 1/7] add a generic soap connector (#60836)

 passerelle/apps/soap/__init__.py              |   0
 .../apps/soap/migrations/0001_initial.py      |  83 +++++
 passerelle/apps/soap/migrations/__init__.py   |   0
 passerelle/apps/soap/models.py                | 182 ++++++++++
 .../templates/soap/soapconnector_detail.html  |   2 +
 passerelle/settings.py                        |   1 +
 tests/test_soap.py                            | 313 +++++++++++++-----
 tests/test_utils_soap.py                      | 109 ++++++
 8 files changed, 613 insertions(+), 77 deletions(-)
 create mode 100644 passerelle/apps/soap/__init__.py
 create mode 100644 passerelle/apps/soap/migrations/0001_initial.py
 create mode 100644 passerelle/apps/soap/migrations/__init__.py
 create mode 100644 passerelle/apps/soap/models.py
 create mode 100644 passerelle/apps/soap/templates/soap/soapconnector_detail.html
 create mode 100644 tests/test_utils_soap.py
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
-