1# -*- coding: utf-8 -*-
2
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the (LGPL) GNU Lesser General Public License as
5# published by the Free Software Foundation; either version 3 of the
6# License, or (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 Library Lesser General Public License for more details at
12# ( http://www.gnu.org/licenses/lgpl.html ).
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )
18
19"""
20Unit tests related to Suds Python library reply processing.
21
22Implemented using the 'pytest' testing framework.
23
24"""
25
26if __name__ == "__main__":
27    import __init__
28    __init__.runUsingPyTest(globals())
29
30
31import suds
32import tests
33
34import pytest
35
36import httplib
37import re
38import xml.sax
39
40
41def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_with_faults():
42    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
43    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
44        "status":s})
45    assert f("", None) is None
46    pytest.raises(Exception, f, "", httplib.INTERNAL_SERVER_ERROR)
47    assert f("", httplib.ACCEPTED) is None
48    assert f("", httplib.NO_CONTENT) is None
49    assert f("bla-bla", httplib.ACCEPTED) is None
50    assert f("bla-bla", httplib.NO_CONTENT) is None
51
52
53def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_without_faults():
54    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
55    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
56        "status":s})
57    assert f("", None) is not None
58    assert f("", httplib.INTERNAL_SERVER_ERROR) is not None
59    assert f("", httplib.ACCEPTED) is None
60    assert f("", httplib.NO_CONTENT) is None
61    assert f("bla-bla", httplib.ACCEPTED) is None
62    assert f("bla-bla", httplib.NO_CONTENT) is None
63
64
65def test_badly_formed_reply_XML():
66    for faults in (True, False):
67        client = tests.client_from_wsdl(_wsdl__simple, faults=faults)
68        pytest.raises(xml.sax.SAXParseException, client.service.f,
69            __inject={"reply":suds.byte_str("bad food")})
70
71
72# TODO: Update the current restriction type output parameter handling so such
73# parameters get converted to the correct Python data type based on the
74# restriction's underlying data type.
75@pytest.mark.xfail
76def test_restriction_data_types():
77    client_unnamed = tests.client_from_wsdl(tests.wsdl_output("""\
78      <xsd:element name="Elemento">
79        <xsd:simpleType>
80          <xsd:restriction base="xsd:int">
81            <xsd:enumeration value="1" />
82            <xsd:enumeration value="3" />
83            <xsd:enumeration value="5" />
84          </xsd:restriction>
85        </xsd:simpleType>
86      </xsd:element>""", "Elemento"))
87
88    client_named = tests.client_from_wsdl(tests.wsdl_output("""\
89      <xsd:simpleType name="MyType">
90        <xsd:restriction base="xsd:int">
91          <xsd:enumeration value="1" />
92          <xsd:enumeration value="3" />
93          <xsd:enumeration value="5" />
94        </xsd:restriction>
95      </xsd:simpleType>
96      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
97
98    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_output("""\
99      <xsd:simpleType name="MyTypeGeneric">
100        <xsd:restriction base="xsd:int">
101          <xsd:enumeration value="1" />
102          <xsd:enumeration value="2" />
103          <xsd:enumeration value="3" />
104          <xsd:enumeration value="4" />
105          <xsd:enumeration value="5" />
106        </xsd:restriction>
107      </xsd:simpleType>
108      <xsd:simpleType name="MyType">
109        <xsd:restriction base="ns:MyTypeGeneric">
110          <xsd:enumeration value="1" />
111          <xsd:enumeration value="3" />
112          <xsd:enumeration value="5" />
113        </xsd:restriction>
114      </xsd:simpleType>
115      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
116
117    for client in (client_unnamed, client_named, client_twice_restricted):
118        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
119<?xml version="1.0"?>
120<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
121  <env:Body>
122    <Elemento xmlns="my-namespace">5</Elemento>
123  </env:Body>
124</env:Envelope>""")))
125        assert response.__class__ is int
126        assert response == 5
127
128
129def test_builtin_data_types(monkeypatch):
130    monkeypatch.delitem(locals(), "e", False)
131
132    integer_type_mapping = {
133        "byte":int,
134        "int":int,
135        "integer":int,
136        "long":long,
137        "negativeInteger":int,
138        "nonNegativeInteger":int,
139        "nonPositiveInteger":int,
140        "positiveInteger":int,
141        "short":int,
142        "unsignedByte":int,
143        "unsignedInt":int,
144        "unsignedLong":long,
145        "unsignedShort":int}
146    for tag, type in integer_type_mapping.items():
147        client = tests.client_from_wsdl(tests.wsdl_output("""\
148      <xsd:element name="value" type="xsd:%s" />""" % tag, "value"))
149        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
150<?xml version="1.0"?>
151<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
152  <env:Body>
153    <value xmlns="my-namespace">15</value>
154  </env:Body>
155</env:Envelope>""")))
156        assert response.__class__ is type
157        assert response == 15
158
159    boolean_mapping = {"0":False, "1":True, "false":False, "true":True}
160    client = tests.client_from_wsdl(tests.wsdl_output("""\
161      <xsd:element name="value" type="xsd:boolean" />""", "value"))
162    for value, expected_value in boolean_mapping.items():
163        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
164<?xml version="1.0"?>
165<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
166  <env:Body>
167    <value xmlns="my-namespace">%s</value>
168  </env:Body>
169</env:Envelope>""" % value)))
170        assert response.__class__ is bool
171        assert response == expected_value
172
173    # Suds implements no extra range checking.
174    client = tests.client_from_wsdl(tests.wsdl_output("""\
175      <xsd:element name="value" type="xsd:byte" />""", "value"))
176    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
177<?xml version="1.0"?>
178<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
179  <env:Body>
180    <value xmlns="my-namespace">1500</value>
181  </env:Body>
182</env:Envelope>""")))
183    assert response.__class__ is int
184    assert response == 1500
185
186    #   Suds raises raw Python exceptions when it fails to convert received
187    # response element data to its mapped Python integer data type, according
188    # to the used WSDL schema.
189    client = tests.client_from_wsdl(tests.wsdl_output("""\
190      <xsd:element name="value" type="xsd:int" />""", "value"))
191    e = pytest.raises(ValueError, client.service.f, __inject=dict(
192        reply=suds.byte_str("""\
193<?xml version="1.0"?>
194<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
195  <env:Body>
196    <value xmlns="my-namespace">Fifteen</value>
197  </env:Body>
198</env:Envelope>"""))).value
199    # ValueError instance received here has different string representations
200    # depending on the Python version used:
201    #   Python 2.4:
202    #     "invalid literal for int(): Fifteen"
203    #   Python 2.7.3, 3.2.3:
204    #     "invalid literal for int() with base 10: 'Fifteen'"
205    assert re.match("invalid literal for int\(\)( with base 10)?: ('?)Fifteen"
206        "\\2$", str(e))
207
208    # Suds returns invalid boolean values as None.
209    invalid_boolean_values = ("True", "", "False", "2", "Fedora", "Z", "-1")
210    client = tests.client_from_wsdl(tests.wsdl_output("""\
211      <xsd:element name="value" type="xsd:boolean" />""", "value"))
212    for value in invalid_boolean_values:
213        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
214<?xml version="1.0"?>
215<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
216  <env:Body>
217    <value xmlns="my-namespace">%s</value>
218  </env:Body>
219</env:Envelope>""" % value)))
220        assert response is None
221
222
223def test_disabling_automated_simple_interface_unwrapping():
224    client = tests.client_from_wsdl(tests.wsdl_output("""\
225      <xsd:element name="Wrapper">
226        <xsd:complexType>
227          <xsd:sequence>
228            <xsd:element name="Elemento" type="xsd:string" />
229          </xsd:sequence>
230        </xsd:complexType>
231      </xsd:element>""", "Wrapper"), unwrap=False)
232    assert not _isOutputWrapped(client, "f")
233
234    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
235<?xml version="1.0"?>
236<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
237  <env:Body>
238    <Wrapper xmlns="my-namespace">
239        <Elemento>La-di-da-da-da</Elemento>
240    </Wrapper>
241  </env:Body>
242</env:Envelope>""")))
243
244    assert response.__class__.__name__ == "Wrapper"
245    assert len(response.__class__.__bases__) == 1
246    assert response.__class__.__bases__[0] is suds.sudsobject.Object
247    assert response.Elemento.__class__ is suds.sax.text.Text
248    assert response.Elemento == "La-di-da-da-da"
249
250
251def test_empty_reply():
252    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
253    f = lambda status=None, description=None : client.service.f(__inject=dict(
254        reply=suds.byte_str(), status=status, description=description))
255    status, reason = f()
256    assert status == httplib.OK
257    assert reason is None
258    status, reason = f(httplib.OK)
259    assert status == httplib.OK
260    assert reason is None
261    status, reason = f(httplib.INTERNAL_SERVER_ERROR)
262    assert status == httplib.INTERNAL_SERVER_ERROR
263    assert reason == 'injected reply'
264    status, reason = f(httplib.FORBIDDEN)
265    assert status == httplib.FORBIDDEN
266    assert reason == 'injected reply'
267    status, reason = f(httplib.FORBIDDEN, "kwack")
268    assert status == httplib.FORBIDDEN
269    assert reason == 'kwack'
270
271
272def test_fault_reply_with_unicode_faultstring(monkeypatch):
273    monkeypatch.delitem(locals(), "e", False)
274
275    unicode_string = u"€ Jurko Gospodnetić ČĆŽŠĐčćžšđ"
276    fault_xml = suds.byte_str(u"""\
277<?xml version="1.0"?>
278<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
279  <env:Body>
280    <env:Fault>
281      <faultcode>env:Client</faultcode>
282      <faultstring>%s</faultstring>
283    </env:Fault>
284  </env:Body>
285</env:Envelope>
286""" % unicode_string)
287
288    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
289    inject = dict(reply=fault_xml, status=httplib.INTERNAL_SERVER_ERROR)
290    e = pytest.raises(suds.WebFault, client.service.f, __inject=inject).value
291    assert e.fault.faultstring == unicode_string
292    assert e.document.__class__ is suds.sax.document.Document
293
294    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
295    status, fault = client.service.f(__inject=dict(reply=fault_xml,
296        status=httplib.INTERNAL_SERVER_ERROR))
297    assert status == httplib.INTERNAL_SERVER_ERROR
298    assert fault.faultstring == unicode_string
299
300
301def test_invalid_fault_namespace(monkeypatch):
302    monkeypatch.delitem(locals(), "e", False)
303
304    fault_xml = suds.byte_str("""\
305<?xml version="1.0"?>
306<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="x">
307  <env:Body>
308    <p:Fault>
309      <faultcode>env:Client</faultcode>
310      <faultstring>Dummy error.</faultstring>
311      <detail>
312        <errorcode>ultimate</errorcode>
313      </detail>
314    </p:Fault>
315  </env:Body>
316</env:Envelope>
317""")
318    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
319    inject = dict(reply=fault_xml, status=httplib.OK)
320    e = pytest.raises(Exception, client.service.f, __inject=inject).value
321    assert e.__class__ is Exception
322    assert str(e) == "<faultcode/> not mapped to message part"
323
324    for http_status in (httplib.INTERNAL_SERVER_ERROR,
325        httplib.PAYMENT_REQUIRED):
326        status, reason = client.service.f(__inject=dict(reply=fault_xml,
327            status=http_status, description="trla baba lan"))
328        assert status == http_status
329        assert reason == "trla baba lan"
330
331
332def test_missing_wrapper_response():
333    """
334    Suds library's automatic structure unwrapping should not be applied to
335    interpreting received SOAP Response XML.
336
337    """
338    client = tests.client_from_wsdl(tests.wsdl_output("""\
339      <xsd:element name="Wrapper">
340        <xsd:complexType>
341          <xsd:sequence>
342            <xsd:element name="fResponse" type="xsd:string" />
343          </xsd:sequence>
344        </xsd:complexType>
345      </xsd:element>""", "Wrapper"))
346    assert _isOutputWrapped(client, "f")
347
348    response_with_missing_wrapper = client.service.f(__inject=dict(
349        reply=suds.byte_str("""<?xml version="1.0"?>
350<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
351  <env:Body>
352    <fResponse xmlns="my-namespace">Anything</fResponse>
353  </env:Body>
354</env:Envelope>""")))
355    assert response_with_missing_wrapper is None
356
357
358def test_reply_error_with_detail_with_fault(monkeypatch):
359    monkeypatch.delitem(locals(), "e", False)
360
361    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
362
363    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
364        inject = dict(reply=_fault_reply__with_detail, status=http_status)
365        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
366        e = e.value
367        _test_fault(e.fault, True)
368        assert e.document.__class__ is suds.sax.document.Document
369        assert str(e) == "Server raised fault: 'Dummy error.'"
370
371    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
372        description="quack-quack")
373    e = pytest.raises(Exception, client.service.f, __inject=inject).value
374    assert e.__class__ is Exception
375    assert e.args[0][0] == httplib.BAD_REQUEST
376    assert e.args[0][1] == "quack-quack"
377
378
379def test_reply_error_with_detail_without_fault():
380    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
381
382    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
383        status, fault = client.service.f(__inject=dict(
384            reply=_fault_reply__with_detail, status=http_status))
385        assert status == httplib.INTERNAL_SERVER_ERROR
386        _test_fault(fault, True)
387
388    status, fault = client.service.f(__inject=dict(
389        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST))
390    assert status == httplib.BAD_REQUEST
391    assert fault == "injected reply"
392
393    status, fault = client.service.f(__inject=dict(
394        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
395        description="haleluja"))
396    assert status == httplib.BAD_REQUEST
397    assert fault == "haleluja"
398
399
400def test_reply_error_without_detail_with_fault(monkeypatch):
401    monkeypatch.delitem(locals(), "e", False)
402
403    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
404
405    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
406        inject = dict(reply=_fault_reply__without_detail, status=http_status)
407        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
408        e = e.value
409        _test_fault(e.fault, False)
410        assert e.document.__class__ is suds.sax.document.Document
411        assert str(e) == "Server raised fault: 'Dummy error.'"
412
413    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
414        description="quack-quack")
415    e = pytest.raises(Exception, client.service.f, __inject=inject).value
416    assert e.__class__ is Exception
417    assert e.args[0][0] == httplib.BAD_REQUEST
418    assert e.args[0][1] == "quack-quack"
419
420
421def test_reply_error_without_detail_without_fault():
422    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
423
424    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
425        status, fault = client.service.f(__inject=dict(
426            reply=_fault_reply__without_detail, status=http_status))
427        assert status == httplib.INTERNAL_SERVER_ERROR
428        _test_fault(fault, False)
429
430    status, fault = client.service.f(__inject=dict(
431        reply=_fault_reply__without_detail, status=httplib.BAD_REQUEST,
432        description="kung-fu-fui"))
433    assert status == httplib.BAD_REQUEST
434    assert fault == "kung-fu-fui"
435
436
437def test_simple_bare_and_wrapped_output():
438    # Prepare web service proxies.
439    client_bare = tests.client_from_wsdl(tests.wsdl_output("""\
440      <xsd:element name="fResponse" type="xsd:string" />""", "fResponse"))
441    client_wrapped = tests.client_from_wsdl(tests.wsdl_output("""\
442      <xsd:element name="Wrapper">
443        <xsd:complexType>
444          <xsd:sequence>
445            <xsd:element name="fResponse" type="xsd:string" />
446          </xsd:sequence>
447        </xsd:complexType>
448      </xsd:element>""", "Wrapper"))
449
450    #   Make sure suds library inteprets our WSDL definitions as wrapped or
451    # bare output interfaces as expected.
452    assert not _isOutputWrapped(client_bare, "f")
453    assert _isOutputWrapped(client_wrapped, "f")
454
455    #   Both bare & wrapped single parameter output web service operation
456    # results get presented the same way even though the wrapped one actually
457    # has an extra wrapper element around its received output data.
458    data = "The meaning of life."
459    get_response = lambda client, x : client.service.f(__inject=dict(
460        reply=suds.byte_str(x)))
461
462    response_bare = get_response(client_bare, """<?xml version="1.0"?>
463<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
464  <env:Body>
465    <fResponse xmlns="my-namespace">%s</fResponse>
466  </env:Body>
467</env:Envelope>""" % data)
468    assert response_bare.__class__ is suds.sax.text.Text
469    assert response_bare == data
470
471    response_wrapped = get_response(client_wrapped, """<?xml version="1.0"?>
472<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
473  <env:Body>
474    <Wrapper xmlns="my-namespace">
475      <fResponse>%s</fResponse>
476    </Wrapper>
477  </env:Body>
478</env:Envelope>""" % data)
479    assert response_wrapped.__class__ is suds.sax.text.Text
480    assert response_wrapped == data
481
482
483def test_wrapped_sequence_output():
484    client = tests.client_from_wsdl(tests.wsdl_output("""\
485      <xsd:element name="Wrapper">
486        <xsd:complexType>
487          <xsd:sequence>
488            <xsd:element name="result1" type="xsd:string" />
489            <xsd:element name="result2" type="xsd:string" />
490            <xsd:element name="result3" type="xsd:string" />
491          </xsd:sequence>
492        </xsd:complexType>
493      </xsd:element>""", "Wrapper"))
494    assert _isOutputWrapped(client, "f")
495
496    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
497<?xml version="1.0"?>
498<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
499  <env:Body>
500    <Wrapper xmlns="my-namespace">
501        <result1>Uno</result1>
502        <result2>Due</result2>
503        <result3>Tre</result3>
504    </Wrapper>
505  </env:Body>
506</env:Envelope>""")))
507
508    #   Composite replies always get unmarshalled as a dynamically constructed
509    # class named 'reply'.
510    assert len(response.__class__.__bases__) == 1
511    assert response.__class__.__name__ == "reply"
512    assert response.__class__.__bases__[0] is suds.sudsobject.Object
513
514    # Check response content.
515    assert len(response) == 3
516    assert response.result1 == "Uno"
517    assert response.result2 == "Due"
518    assert response.result3 == "Tre"
519    assert response.result1.__class__ is suds.sax.text.Text
520    assert response.result2.__class__ is suds.sax.text.Text
521    assert response.result3.__class__ is suds.sax.text.Text
522
523
524def _attibutes(object):
525    result = set()
526    for x in object:
527        result.add(x[0])
528    return result
529
530
531def _isOutputWrapped(client, method_name):
532    assert len(client.wsdl.bindings) == 1
533    operation = client.wsdl.bindings.values()[0].operations[method_name]
534    return operation.soap.output.body.wrapped
535
536
537def _test_fault(fault, has_detail):
538    assert fault.faultcode == "env:Client"
539    assert fault.faultstring == "Dummy error."
540    assert hasattr(fault, "detail") == has_detail
541    assert not has_detail or fault.detail.errorcode == "ultimate"
542    assert not hasattr(fault, "nonexisting")
543    expected_attributes = set(("faultcode", "faultstring"))
544    if has_detail:
545        expected_attributes.add("detail")
546    assert _attibutes(fault) == expected_attributes
547    assert not has_detail or _attibutes(fault.detail) == set(("errorcode",))
548
549
550_fault_reply__with_detail = suds.byte_str("""\
551<?xml version="1.0"?>
552<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
553  <env:Body>
554    <env:Fault>
555      <faultcode>env:Client</faultcode>
556      <faultstring>Dummy error.</faultstring>
557      <detail>
558        <errorcode>ultimate</errorcode>
559      </detail>
560    </env:Fault>
561  </env:Body>
562</env:Envelope>
563""")
564
565_fault_reply__without_detail = suds.byte_str("""\
566<?xml version="1.0"?>
567<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
568  <env:Body>
569    <env:Fault>
570      <faultcode>env:Client</faultcode>
571      <faultstring>Dummy error.</faultstring>
572    </env:Fault>
573  </env:Body>
574</env:Envelope>
575""")
576
577_wsdl__simple = tests.wsdl_output("""\
578      <xsd:element name="fResponse">
579        <xsd:complexType>
580          <xsd:sequence>
581            <xsd:element name="output_i" type="xsd:integer" />
582            <xsd:element name="output_s" type="xsd:string" />
583          </xsd:sequence>
584        </xsd:complexType>
585      </xsd:element>""", "fResponse")
586