From 134640f97c596bcee484f37a9cd7818f5d54d5a1 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 15 Oct 2019 10:39:01 +0200
Subject: [PATCH 1/2] utils/soap: allow zeep versions 2 and 3 (#35370)

---
 passerelle/contrib/iparapheur/models.py | 13 ++++++---
 setup.py                                |  2 +-
 tests/data/soap.wsdl                    |  5 ++--
 tests/test_iparapheur.py                | 28 ++++++++++++++-----
 tests/test_soap.py                      | 36 +++++++++++++++++++++++++
 tox.ini                                 |  4 ++-
 6 files changed, 75 insertions(+), 13 deletions(-)

diff --git a/passerelle/contrib/iparapheur/models.py b/passerelle/contrib/iparapheur/models.py
index 40f607b4..d04293f9 100644
--- a/passerelle/contrib/iparapheur/models.py
+++ b/passerelle/contrib/iparapheur/models.py
@@ -23,6 +23,10 @@ from django.utils.translation import ugettext_lazy as _
 from django.http import HttpResponse, Http404
 
 from zeep.exceptions import Fault as WebFault, TransportError, XMLSyntaxError
+try:
+    from zeep import Settings  # zeep version >= 3.x
+except ImportError:
+    Settings = None
 
 from passerelle.base.models import BaseResource, HTTPResource
 from passerelle.utils.api import endpoint
@@ -97,9 +101,9 @@ class IParapheur(BaseResource, HTTPResource):
     def get_verbose_name(cls):
         return cls._meta.verbose_name
 
-    def get_client(self, strict_mode=True):
+    def get_client(self, **kwargs):
         try:
-            soap_client = self.soap_client(strict=strict_mode)
+            soap_client = self.soap_client(**kwargs)
 
             # overrides the service port address URL defined in the WSDL.
             if self.wsdl_endpoint_location:
@@ -115,7 +119,10 @@ class IParapheur(BaseResource, HTTPResource):
 
     def call(self, service_name, *args, **kwargs):
         strict_mode = kwargs.pop('strict_mode', True)
-        client = self.get_client(strict_mode=strict_mode)
+        if Settings is not None:  # zeep version >= 3.x:
+            client = self.get_client(settings=Settings(strict=strict_mode))
+        else:
+            client = self.get_client(strict=strict_mode)
         try:
             result = getattr(client.overridden_service, service_name)(*args, **kwargs)
         except WebFault as exc:
diff --git a/setup.py b/setup.py
index 1820b6d2..8b002d7b 100755
--- a/setup.py
+++ b/setup.py
@@ -104,7 +104,7 @@ setup(name='passerelle',
             'python-dateutil',
             'Pillow',
             'jsonschema < 3.1',
-            'zeep < 3.0',
+            'zeep',
             'pycrypto',
             'unidecode',
             'paramiko',
diff --git a/tests/data/soap.wsdl b/tests/data/soap.wsdl
index 57832709..399af5a1 100644
--- a/tests/data/soap.wsdl
+++ b/tests/data/soap.wsdl
@@ -48,9 +48,10 @@
       </element>
       <element name="TradePrice">
         <complexType>
-          <all>
+          <sequence>
+            <element name="skipMe" type="float"/>
             <element name="price" type="float"/>
-          </all>
+          </sequence>
         </complexType>
       </element>
       <complexType name="account">
diff --git a/tests/test_iparapheur.py b/tests/test_iparapheur.py
index eefa61bd..605f14b0 100644
--- a/tests/test_iparapheur.py
+++ b/tests/test_iparapheur.py
@@ -8,6 +8,7 @@ import base64
 import xml.etree.ElementTree as ET
 from dateutil import parser
 from requests import Response
+from zeep import __version__ as zeep_version
 
 from requests.exceptions import ConnectionError
 from django.core.urlresolvers import reverse
@@ -41,6 +42,12 @@ def conn():
                     resource_pk=conn.pk)
     return conn
 
+def assert_invalid_xml(err_desc):
+    if zeep_version < '3':
+        assert "Server returned HTTP status 200 (<nada>)" in err_desc
+    else:
+        assert "Server returned response (200) with invalid XML" in err_desc
+
 def xmlmime():
     return os.path.join(os.path.dirname(__file__), 'data','xmlmime.xml')
 
@@ -70,7 +77,16 @@ def iph_mocked_get(url, params=None, **kwargs):
 def test_call_ping(soap_client, app, conn):
     service = mock.Mock()
     service.echo.return_value = 'pong'
-    mocked_client = mock.Mock(overridden_service=service)
+
+    class MockedSettings(object):
+        def __init__(self, **kwargs):
+            pass
+        def __enter__(self):
+            pass
+        def __exit__(self, exc_type, exc_value, traceback):
+            pass
+
+    mocked_client = mock.Mock(overridden_service=service, settings=MockedSettings)
     soap_client.return_value = mocked_client
     url = reverse('generic-endpoint', kwargs={'connector': 'iparapheur',
                     'endpoint': 'ping', 'slug': conn.slug})
@@ -187,7 +203,7 @@ def test_create_file(mocked_post, mocked_get, app, conn):
     assert (BASE_URL,) == mocked_post.call_args[0]
     assert resp.json['err'] == 1
     assert 'zeep.exceptions.TransportError' in resp.json['err_class']
-    assert 'Server returned HTTP status 200 (<nada>)' in resp.json['err_desc']
+    assert_invalid_xml(resp.json['err_desc'])
 
     # Unknown value for "visibility"
     err_data = data.copy()
@@ -211,7 +227,7 @@ def test_create_file(mocked_post, mocked_get, app, conn):
     assert (BASE_URL,) == mocked_post.call_args[0]
     assert resp.json['err'] == 1
     assert 'zeep.exceptions.TransportError' in resp.json['err_class']
-    assert 'Server returned HTTP status 200 (<nada>)' in resp.json['err_desc']
+    assert_invalid_xml(resp.json['err_desc'])
 
 @mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
 @mock.patch('passerelle.utils.Request.post')
@@ -300,7 +316,7 @@ def test_get_file_status(mocked_post, mocked_get, app, conn):
     assert resp.json['err'] == 1
     #assert 'zeep.exceptions.TransportError' in resp.json['err_class']
     assert 'passerelle.utils.jsonresponse.APIError' in resp.json['err_class']
-    assert 'Server returned HTTP status 200 (<nada>)' in resp.json['err_desc']
+    assert_invalid_xml(resp.json['err_desc'])
 
 @mock.patch('passerelle.utils.Request.get', side_effect=iph_mocked_get)
 @mock.patch('passerelle.utils.Request.post')
@@ -325,7 +341,7 @@ def test_get_file(mocked_post, mocked_get, app, conn):
     assert resp.body == 'Test Document'
 
     # KO
-    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><CreerDossierResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"><MessageRetour><codeRetour>KO</codeRetour><message>KOmessage</message><severite>INFO</severite></MessageRetour></CreerDossierResponse></S:Body></S:Envelope>"""
+    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><GetDossierResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"><MessageRetour><codeRetour>KO</codeRetour><message>KOmessage</message><severite>INFO</severite></MessageRetour></GetDossierResponse></S:Body></S:Envelope>"""
     response._content = soap_response
     mocked_post.return_value = response
     resp = app.get(url, status=500)
@@ -334,7 +350,7 @@ def test_get_file(mocked_post, mocked_get, app, conn):
     assert resp.json['err_desc'] == 'KOmessage'
 
     # unknown response
-    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><CreerDossierResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"></CreerDossierResponse></S:Body></S:Envelope>"""
+    soap_response = """<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><GetDossierResponse xmlns="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime"></GetDossierResponse></S:Body></S:Envelope>"""
     response._content = soap_response
     mocked_post.return_value = response
     resp = app.get(url, status=500)
diff --git a/tests/test_soap.py b/tests/test_soap.py
index 7752707f..b8bc7a59 100644
--- a/tests/test_soap.py
+++ b/tests/test_soap.py
@@ -1,5 +1,12 @@
+import pytest
+import mock
 import requests
 from zeep.plugins import Plugin
+from zeep.exceptions import XMLParseError
+try:
+    from zeep import Settings  # zeep version >= 3.x
+except ImportError:
+    Settings = None
 
 from passerelle.utils.soap import SOAPClient
 
@@ -30,3 +37,32 @@ def test_soap_client():
     assert client.transport.session == soap_resource.requests
     assert client.transport.cache
     assert client.plugins == plugins
+
+@mock.patch('requests.sessions.Session.post')
+def test_disable_strict_mode(mocked_post):
+    response = requests.Response()
+    response.status_code = 200
+    response._content = '''<?xml version='1.0' encoding='utf-8'?>
+<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
+  <soap-env:Body>
+    <ns0:TradePrice xmlns:ns0="http://example.com/stockquote.xsd">
+      <price>4.20</price>
+    </ns0:TradePrice>
+  </soap-env:Body>
+</soap-env:Envelope>'''
+    mocked_post.return_value = response
+
+    soap_resource = SOAPResource()
+    client = SOAPClient(soap_resource)
+    with pytest.raises(
+            XMLParseError, match="Unexpected element u'price', expected u'skipMe'"):
+        client.service.GetLastTradePrice(tickerSymbol='banana')
+
+    if Settings is not None:  # zeep version >= 3.x:
+        client = SOAPClient(soap_resource, settings=Settings(strict=False))
+    else:
+        client = SOAPClient(soap_resource, strict=False)
+    result = client.service.GetLastTradePrice(tickerSymbol='banana')
+    assert len(result) == 2
+    assert result['skipMe'] == None
+    assert result['price'] == 4.2
diff --git a/tox.ini b/tox.ini
index 4779c8bb..9a218c2e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/passerelle/{env:BRANCH_NAME:}
-envlist = django111-pg
+envlist = django111-pg-zeep2
 
 [testenv]
 usedevelop = True
@@ -36,6 +36,8 @@ deps =
   vobject
   django-ratelimit
   pyquery
+  zeep2: zeep < 3.0
+  zeep3: zeep >= 3.0
 commands =
   ./get_wcs.sh
   django111: py.test {posargs: {env:FAST:} --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=passerelle/ --cov-config .coveragerc tests/}
-- 
2.24.1

