1# -*- coding: utf-8 -*-
2
3# This program is free software; you can redistribute it and/or modify it under
4# the terms of the (LGPL) GNU Lesser General Public License as published by the
5# Free Software Foundation; either version 3 of the License, or (at your
6# option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but WITHOUT
9# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
11# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )
17
18"""
19Suds Python library web service operation argument parser related unit tests.
20
21Suds library prepares web service operation invocation functions that construct
22actual web service operation invocation requests based on the parameters they
23receive and their web service operation's definition.
24
25The module tested here implements generic argument parsing and validation, not
26specific to a particular web service operation binding.
27
28"""
29
30import testutils
31if __name__ == "__main__":
32    testutils.run_using_pytest(globals())
33
34import suds
35import suds.argparser
36
37import pytest
38
39
40class MockAncestor:
41    """
42    Represents a web service operation parameter ancestry item.
43
44    Implements parts of the suds library's web service operation ancestry item
45    interface required by the argument parser functionality.
46
47    """
48
49    def __init__(self, is_choice=False):
50        self.__is_choice = is_choice
51
52    def choice(self):
53        return self.__is_choice
54
55
56class MockParamProcessor:
57    """
58    Mock parameter processor that gets passed argument parsing results.
59
60    Collects received parameter information so it may be checked after argument
61    parsing has completed.
62
63    """
64
65    def __init__(self):
66        self.params_ = []
67
68    def params(self):
69        return self.params_
70
71    def process(self, param_name, param_type, in_choice_context, value):
72        self.params_.append((param_name, param_type, in_choice_context, value))
73
74
75class MockParamType:
76    """
77    Represents a web service operation parameter type.
78
79    Implements parts of the suds library's web service operation parameter type
80    interface required by the argument parsing implementation tested in this
81    module.
82
83    """
84
85    def __init__(self, optional):
86        self.optional_ = optional
87
88    def optional(self):
89        return self.optional_
90
91
92@pytest.mark.parametrize("binding_style", (
93    "document",
94    #TODO: Suds library's RPC binding implementation should be updated to use
95    # the argument parsing functionality. This will remove code duplication
96    # between different binding implementations and make their features more
97    # balanced.
98    pytest.param("rpc", marks=pytest.mark.xfail(reason="Not yet implemented.")),
99    ))
100def test_binding_uses_argument_parsing(monkeypatch, binding_style):
101    """
102    Calling web service operations should use the generic argument parsing
103    functionality independent of the operation's specific binding style.
104
105    """
106    class MyException(Exception):
107        pass
108    def raise_exception(*args, **kwargs):
109        raise MyException
110    monkeypatch.setattr(suds.argparser._ArgParser, "__init__", raise_exception)
111
112    wsdl = suds.byte_str("""\
113<?xml version='1.0' encoding='UTF-8'?>
114<wsdl:definitions targetNamespace="my-namespace"
115xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
116xmlns:ns="my-namespace"
117xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
118  <wsdl:types>
119    <xsd:schema targetNamespace="my-namespace"
120    elementFormDefault="qualified"
121    attributeFormDefault="unqualified"
122    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
123      <xsd:element name="Bongo" type="xsd:string" />
124    </xsd:schema>
125  </wsdl:types>
126  <wsdl:message name="fRequestMessage">"
127    <wsdl:part name="parameters" element="ns:Bongo" />
128  </wsdl:message>
129  <wsdl:portType name="dummyPortType">
130    <wsdl:operation name="f">
131      <wsdl:input message="ns:fRequestMessage" />
132    </wsdl:operation>
133  </wsdl:portType>
134  <wsdl:binding name="dummy" type="ns:dummyPortType">
135    <soap:binding style="document"
136    transport="http://schemas.xmlsoap.org/soap/http" />
137    <wsdl:operation name="f">
138      <soap:operation soapAction="my-soap-action" style="%s" />
139      <wsdl:input><soap:body use="literal" /></wsdl:input>
140    </wsdl:operation>
141  </wsdl:binding>
142  <wsdl:service name="dummy">
143    <wsdl:port name="dummy" binding="ns:dummy">
144      <soap:address location="unga-bunga-location" />
145    </wsdl:port>
146  </wsdl:service>
147</wsdl:definitions>
148""" % (binding_style,))
149    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
150    pytest.raises(MyException, client.service.f)
151    pytest.raises(MyException, client.service.f, "x")
152    pytest.raises(MyException, client.service.f, "x", "y")
153
154
155@pytest.mark.parametrize("binding_style", (
156    "document",
157    #TODO: Suds library's RPC binding implementation should be updated to use
158    # the argument parsing functionality. This will remove code duplication
159    # between different binding implementations and make their features more
160    # balanced.
161    pytest.param("rpc", marks=pytest.mark.xfail(reason="Not yet implemented.")),
162    ))
163def test_binding_for_an_operation_with_no_input_uses_argument_parsing(
164        monkeypatch, binding_style):
165    """
166    Calling web service operations should use the generic argument parsing
167    functionality independent of the operation's specific binding style.
168
169    """
170    class MyException(Exception):
171        pass
172    def raise_exception(*args, **kwargs):
173        raise MyException
174    monkeypatch.setattr(suds.argparser._ArgParser, "__init__", raise_exception)
175
176    wsdl = suds.byte_str("""\
177<?xml version='1.0' encoding='UTF-8'?>
178<wsdl:definitions targetNamespace="my-namespace"
179xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
180xmlns:ns="my-namespace"
181xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
182  <wsdl:portType name="dummyPortType">
183    <wsdl:operation name="f" />
184  </wsdl:portType>
185  <wsdl:binding name="dummy" type="ns:dummyPortType">
186    <soap:binding style="document"
187    transport="http://schemas.xmlsoap.org/soap/http" />
188    <wsdl:operation name="f">
189      <soap:operation soapAction="my-soap-action" style="%s" />
190    </wsdl:operation>
191  </wsdl:binding>
192  <wsdl:service name="dummy">
193    <wsdl:port name="dummy" binding="ns:dummy">
194      <soap:address location="unga-bunga-location" />
195    </wsdl:port>
196  </wsdl:service>
197</wsdl:definitions>
198""" % (binding_style,))
199    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
200    pytest.raises(MyException, client.service.f)
201    pytest.raises(MyException, client.service.f, "x")
202    pytest.raises(MyException, client.service.f, "x", "y")
203
204
205@pytest.mark.parametrize(("param_optional", "args"), (
206    # Operations taking no parameters.
207    ((), (1,)),
208    ((), (1, 2)),
209    ((), (1, 2, None)),
210    # Operations taking a single parameter.
211    ((True,), (1, 2)),
212    ((False,), (1, 2)),
213    ((True,), ("2", 2, None)),
214    ((False,), ("2", 2, None)),
215    ((True,),  (object(), 2, None, None)),
216    ((False,), (object(), 2, None, None)),
217    ((True,), (None, 2, None, None, "5")),
218    ((False,), (None, 2, None, None, "5")),
219    # Operations taking multiple parameters.
220    ((True, True), (1, 2, 3)),
221    ((False, True), (1, 2, 3)),
222    ((True, False), (1, 2, 3)),
223    ((False, False), (1, 2, 3)),
224    ((False, True), ("2", 2, None)),
225    ((False, False), ("2", 2, None)),
226    ((True, True), ("2", 2, None)),
227    ((True, True, True), (object(), 2, None, None)),
228    ((False, False, False), (object(), 2, None, None)),
229    ((True, False, False), (None, 2, None, None, "5")),
230    ((True, False, True), (None, 2, None, None, "5")),
231    ((True, True, True), (None, 2, None, None, "5"))))
232def test_extra_positional_arguments(param_optional, args):
233    """
234    Test passing extra positional arguments for an operation expecting more
235    than one.
236
237    """
238    param_count = len(param_optional)
239    params = []
240    expected_args_min = 0
241    for i, optional in enumerate(param_optional):
242        if not optional:
243            expected_args_min += 1
244        param_name = "p%d" % (i,)
245        param_type = MockParamType(optional)
246        params.append((param_name, param_type))
247    param_processor = MockParamProcessor()
248
249    takes_plural_suffix = "s"
250    if expected_args_min == param_count:
251        takes = param_count
252        if param_count == 1:
253            takes_plural_suffix = ""
254    else:
255        takes = "%d to %d" % (expected_args_min, param_count)
256    was_were = "were"
257    if len(args) == 1:
258        was_were = "was"
259    expected = "fru-fru() takes %s positional argument%s but %d %s given" % (
260        takes, takes_plural_suffix, len(args), was_were)
261    _expect_error(TypeError, expected, suds.argparser.parse_args, "fru-fru",
262        params, args, {}, param_processor.process, True)
263
264    assert len(param_processor.params()) == param_count
265    processed_params = param_processor.params()
266    for expected_param, param, value in zip(params, processed_params, args):
267        assert param[0] is expected_param[0]
268        assert param[1] is expected_param[1]
269        assert not param[2]
270        assert param[3] is value
271
272
273@pytest.mark.parametrize(("param_names", "args", "kwargs"), (
274    (["a"], (1,), {"a": 5}),
275    ([["a"]], (1,), {"a": 5}),
276    (["a"], (None, 1, 2, 7), {"a": 5}),
277    ([["a"]], (None, 1, 2, 7), {"a": 5}),
278    (["a", ["b"], "c"], (None, None, None), {"a": 1, "b": 2, "c": 3}),
279    ([["a"], ["b"], ["c"]], (None, None, None), {"a": 1, "b": 2, "c": 3}),
280    (["a"], ("x",), {"a": None}),
281    (["a", ["b"], ["c"]], (1,), {"a": None}),
282    (["a", "b", ["c"]], (None, 2), {"b": None})))
283def test_multiple_value_for_single_parameter_error(param_names, args, kwargs):
284    """
285    Test how multiple value for a single parameter errors are reported.
286
287    This report takes precedence over any extra positional argument errors.
288
289    Optional parameters are marked by specifying their names as single element
290    lists or tuples.
291
292    """
293    params = []
294    duplicates = []
295    args_count = len(args)
296    for n, param_name in enumerate(param_names):
297        optional = False
298        if param_name.__class__ in (tuple, list):
299            optional = True
300            param_name = param_name[0]
301        if n < args_count and param_name in kwargs:
302            duplicates.append(param_name)
303        params.append((param_name, MockParamType(optional)))
304    message = "q() got multiple values for parameter '%s'"
305    expected = [message % (x,) for x in duplicates]
306    if len(expected) == 1:
307        expected = expected[0]
308    _expect_error(TypeError, expected, suds.argparser.parse_args, "q", params,
309        args, kwargs, _do_nothing, True)
310
311
312def test_not_reporting_extra_argument_errors():
313    """
314    When ArgParser has been configured not to report extra argument errors as
315    exceptions, it should simply ignore any such extra arguments. This matches
316    the suds library behaviour from before extra argument error reporting was
317    implemented.
318
319    """
320    x = MockAncestor()
321    c = MockAncestor(is_choice=True)
322    params = [
323        ("p1", MockParamType(False), [x]),
324        ("p2", MockParamType(True), [x, c]),
325        ("p3", MockParamType(False), [x, c])]
326    args = list(range(5))
327    kwargs = {"p1": "p1", "p3": "p3", "x": 666}
328    param_processor = MockParamProcessor()
329    args_required, args_allowed = suds.argparser.parse_args("w", params, args,
330        kwargs, param_processor.process, False)
331
332    assert args_required == 1
333    assert args_allowed == 3
334    processed_params = param_processor.params()
335    assert len(processed_params) == len(params)
336    for expected_param, param, value in zip(params, processed_params, args):
337        assert param[0] is expected_param[0]
338        assert param[1] is expected_param[1]
339        assert param[2] == (c in expected_param[2])
340        assert param[3] is value
341
342
343@pytest.mark.parametrize(("param_names", "args", "kwargs"), (
344    ([], (), {"x": 5}),
345    ([], (None, 1, 2, 7), {"x": 5}),
346    ([], (), {"x": 1, "y": 2, "z": 3}),
347    (["a"], (), {"x": None}),
348    ([["a"]], (), {"x": None}),
349    (["a"], (1,), {"x": None}),
350    ([["a"]], (1,), {"x": None}),
351    (["a"], (), {"a": "spank me", "x": 5}),
352    (["a"], (), {"x": 5, "a": "spank me"}),
353    (["a"], (), {"a": "spank me", "x": 5, "wuwu": None}),
354    (["a", "b", "c"], (1, 2), {"w": 666}),
355    (["a", ["b"], ["c"]], (1,), {"c": None, "w": 666}),
356    (["a", "b", ["c"]], (None,), {"b": None, "_": 666})))
357def test_unexpected_keyword_argument(param_names, args, kwargs):
358    """
359    Test how unexpected keyword arguments are reported.
360
361    This report takes precedence over any extra positional argument errors.
362
363    Optional parameters are marked by specifying their names as single element
364    lists or tuples.
365
366    """
367    params = []
368    arg_count = len(args)
369    for n, param_name in enumerate(param_names):
370        optional = False
371        if param_name.__class__ in (tuple, list):
372            optional = True
373            param_name = param_name[0]
374        if n < arg_count:
375            assert param_name not in kwargs
376        else:
377            kwargs.pop(param_name, None)
378        params.append((param_name, MockParamType(optional)))
379    message = "pUFf() got an unexpected keyword argument '%s'"
380    expected = [message % (x,) for x in kwargs]
381    if len(expected) == 1:
382        expected = expected[0]
383    _expect_error(TypeError, expected, suds.argparser.parse_args, "pUFf",
384        params, args, kwargs, _do_nothing, True)
385
386
387@pytest.mark.parametrize(("expect_required", "expect_allowed", "param_defs"), (
388    # No parameters.
389    (0, 0, []),
390    # Single parameter.
391    (1, 1, [("p1", False, [1, 2, 3, 4])]),
392    (0, 1, [("p1", True, [1, 2, 3, 4])]),
393    (1, 1, [("p1", False, [1, 2, 3, [4]])]),
394    (0, 1, [("p1", True, [1, 2, 3, [4]])]),
395    (1, 1, [("p1", False, [1, [2], 3, 4])]),
396    (0, 1, [("p1", True, [1, [2], 3, 4])]),
397    (1, 1, [("p1", False, [1, [2], 3, [4]])]),
398    (0, 1, [("p1", True, [1, [2], 3, [4]])]),
399    # Multiple parameters.
400    (4, 4, [
401        ("a", False, [1]),
402        ("b", False, [1]),
403        ("c", False, [1]),
404        ("d", False, [1])]),
405    (0, 4, [
406        ("a", True, [1]),
407        ("b", True, [1]),
408        ("c", True, [1]),
409        ("d", True, [1])]),
410    (2, 4, [
411        ("a", True, [1]),
412        ("b", False, [1]),
413        ("c", True, [1]),
414        ("d", False, [1])]),
415    (2, 4, [
416        ("a", False, [1]),
417        ("b", True, [1]),
418        ("c", False, [1]),
419        ("d", True, [1])]),
420    (3, 4, [
421        ("a", False, [1]),
422        ("b", False, [1]),
423        ("c", False, [1]),
424        ("d", True, [1])]),
425    (3, 4, [
426        ("a", True, [1]),
427        ("b", False, [1]),
428        ("c", False, [1]),
429        ("d", False, [1])]),
430    # Choice containing only simple members.
431    (1, 2, [
432        ("a", False, [[1]]),
433        ("b", False, [[1]])]),
434    (0, 2, [
435        ("a", True, [[1]]),
436        ("b", False, [[1]])]),
437    (0, 2, [
438        ("a", False, [[1]]),
439        ("b", True, [[1]])]),
440    (0, 2, [
441        ("a", True, [[1]]),
442        ("b", True, [[1]])]),
443    # Choice containing a non-empty sequence.
444    (1, 3, [
445        ("a", False, [1, 2, 3, [4]]),
446        ("b1", False, [1, 2, 3, [4], 5]),
447        ("b2", False, [1, 2, 3, [4], 5])]),
448    # Choice with more than one required parameter.
449    (2, 4, [
450        ("a1", False, [[1], 2]),
451        ("a2", False, [[1], 2]),
452        ("b1", False, [[1], 3]),
453        ("b2", False, [[1], 3])]),
454    (2, 5, [
455        ("a1", False, [[1], 2]),
456        ("a2", False, [[1], 2]),
457        ("b1", False, [[1], 3]),
458        ("b2", False, [[1], 3]),
459        ("b3", False, [[1], 3])]),
460    (2, 5, [
461        ("a1", False, [[1], 2]),
462        ("a2", False, [[1], 2]),
463        ("a3", False, [[1], 2]),
464        ("b1", False, [[1], 3]),
465        ("b2", False, [[1], 3])]),
466    (3, 6, [
467        ("a1", False, [[1], 2]),
468        ("a2", False, [[1], 2]),
469        ("a3", False, [[1], 2]),
470        ("b1", False, [[1], 3]),
471        ("b2", False, [[1], 3]),
472        ("b3", False, [[1], 3])]),
473    (2, 6, [
474        ("a1", False, [[1], 2]),
475        ("a2", True, [[1], 2]),
476        ("a3", False, [[1], 2]),
477        ("b1", False, [[1], 3]),
478        ("b2", False, [[1], 3]),
479        ("b3", False, [[1], 3])]),
480    # Sequence containing multiple choices.
481    (2, 4, [
482        ("a1", False, [0, [1]]),
483        ("a2", False, [0, [1]]),
484        ("b1", False, [0, [2]]),
485        ("b2", False, [0, [2]])]),
486    (1, 4, [
487        ("a1", False, [0, [1]]),
488        ("a2", False, [0, [1]]),
489        ("b1", False, [0, [2]]),
490        ("b2", True, [0, [2]])]),
491    (3, 5, [
492        ("a1", False, [0, [1]]),
493        ("a2", False, [0, [1]]),
494        ("x", False, [0]),
495        ("b1", False, [0, [2]]),
496        ("b2", False, [0, [2]])]),
497    # Choice containing optional parameters.
498    (0, 3, [
499        ("a", False, [1, [2]]),
500        ("b", True, [1, [2]]),
501        ("c", False, [1, [2]])]),
502    (0, 3, [
503        ("a", False, [1, [2]]),
504        ("b1", True, [1, [2], 3]),
505        ("b2", True, [1, [2], 3])]),
506    (1, 3, [
507        ("a", False, [1, [2]]),
508        ("b1", False, [1, [2], 3]),
509        ("b2", True, [1, [2], 3])]),
510    # Choices within choices next to choices.
511    (3, 14, [
512        ("p01", False, [1]),
513        ("p02", False, [1, [2], 3]),
514        ("p03", False, [1, [2], 3]),
515        ("p04", False, [1, [2], 4, 5, 6]),
516        ("p05", False, [1, [2], 4, 5, 6, [7]]),
517        ("p06", False, [1, [2], 4, 5, 6, [7], [8]]),
518        ("p07", False, [1, [2], 4, 5, 6, [7], 9]),
519        ("p08", False, [1, [2], 4, 5, 6, [7], 9]),
520        ("p09", False, [1, [2], 4, [10], 11]),
521        ("p10", False, [1, [2], 4, [10], 11]),
522        ("p11", False, [1, [2], 4, [10], 12]),
523        ("p12", False, [1, [2], 4, [10], 12]),
524        ("p13", False, [1, [2], 4, [10], 12]),
525        ("p14", False, [1, [2], 4, [13]])]),
526    ))
527def test_unwrapped_arg_counts(expect_required, expect_allowed, param_defs):
528    """
529    Test required & allowed argument count for unwrapped parameters.
530
531    Expected 'param_defs' structure - list of 3-tuples containing the
532    following:
533      * Parameter name (string).
534      * Optional (boolean).
535      * Ancestry (list).
536        * Contains integers and/or single element lists containing an integer.
537          * Integers represent non-choice ancestry items.
538          * Single element lists represent choice ancestry items.
539        * Integer values represent ancestry item ids - different integer values
540          represent separate ancestry items.
541
542    """
543    ancestor_map = {}
544    params = []
545    for param_name, param_optional, param_ancestry_def in param_defs:
546        ancestry = []
547        for n, id in enumerate(param_ancestry_def):
548            is_choice = False
549            if id.__class__ is list:
550                assert len(id) == 1, "bad test input"
551                id = id[0]
552                is_choice = True
553            try:
554                ancestor, ancestry_def = ancestor_map[id]
555            except KeyError:
556                ancestor = MockAncestor(is_choice)
557                ancestor_map[id] = (ancestor, param_ancestry_def[:n])
558            else:
559                assert ancestor.choice() == is_choice, "bad test input"
560                assert ancestry_def == param_ancestry_def[:n], "bad test input"
561            ancestry.append(ancestor)
562        params.append((param_name, MockParamType(param_optional), ancestry))
563    param_processor = MockParamProcessor()
564    args = [object() for x in params]
565    args_required, args_allowed = suds.argparser.parse_args("w", params, args,
566        {}, param_processor.process, False)
567
568    assert args_required == expect_required
569    assert args_allowed == expect_allowed
570    processed_params = param_processor.params()
571    assert len(processed_params) == len(params)
572    for expected_param, param, value in zip(params, processed_params, args):
573        assert param[0] is expected_param[0]
574        assert param[1] is expected_param[1]
575        expected_in_choice_context = False
576        for x in expected_param[2]:
577            if x.choice():
578                expected_in_choice_context = True
579                break
580        assert param[2] == expected_in_choice_context
581        assert param[3] is value
582
583
584def _do_nothing(*args, **kwargs):
585    """Do-nothing function used as a callback where needed during testing."""
586    pass
587
588
589def _expect_error(expected_exception, expected_error_text, test_function,
590        *args, **kwargs):
591    """
592    Assert a test function call raises an expected exception.
593
594    Caught exception is considered expected if its string representation
595    matches the given expected error text.
596
597    Expected error text may be given directly or as a list/tuple containing
598    valid alternatives.
599
600    """
601    e = pytest.raises(expected_exception, test_function, *args, **kwargs).value
602    try:
603        if expected_error_text.__class__ in (list, tuple):
604            assert str(e) in expected_error_text
605        else:
606            assert str(e) == expected_error_text
607    finally:
608        del e  # explicitly break circular reference chain in Python 3
609