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