| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.web.test.test_xmlrpc -*- | |
| 2 # | |
| 3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 """ | |
| 7 Test XML-RPC support. | |
| 8 """ | |
| 9 | |
| 10 try: | |
| 11 import xmlrpclib | |
| 12 except ImportError: | |
| 13 xmlrpclib = None | |
| 14 class XMLRPC: pass | |
| 15 else: | |
| 16 from twisted.web import xmlrpc | |
| 17 from twisted.web.xmlrpc import XMLRPC, addIntrospection, _QueryFactory | |
| 18 | |
| 19 from twisted.trial import unittest | |
| 20 from twisted.web import server, static, client, error, http | |
| 21 from twisted.internet import reactor, defer | |
| 22 from twisted.internet.error import ConnectionDone | |
| 23 from twisted.python import failure | |
| 24 | |
| 25 | |
| 26 class TestRuntimeError(RuntimeError): | |
| 27 pass | |
| 28 | |
| 29 class TestValueError(ValueError): | |
| 30 pass | |
| 31 | |
| 32 | |
| 33 | |
| 34 class Test(XMLRPC): | |
| 35 | |
| 36 FAILURE = 666 | |
| 37 NOT_FOUND = 23 | |
| 38 SESSION_EXPIRED = 42 | |
| 39 | |
| 40 # the doc string is part of the test | |
| 41 def xmlrpc_add(self, a, b): | |
| 42 """ | |
| 43 This function add two numbers. | |
| 44 """ | |
| 45 return a + b | |
| 46 | |
| 47 xmlrpc_add.signature = [['int', 'int', 'int'], | |
| 48 ['double', 'double', 'double']] | |
| 49 | |
| 50 # the doc string is part of the test | |
| 51 def xmlrpc_pair(self, string, num): | |
| 52 """ | |
| 53 This function puts the two arguments in an array. | |
| 54 """ | |
| 55 return [string, num] | |
| 56 | |
| 57 xmlrpc_pair.signature = [['array', 'string', 'int']] | |
| 58 | |
| 59 # the doc string is part of the test | |
| 60 def xmlrpc_defer(self, x): | |
| 61 """Help for defer.""" | |
| 62 return defer.succeed(x) | |
| 63 | |
| 64 def xmlrpc_deferFail(self): | |
| 65 return defer.fail(TestValueError()) | |
| 66 | |
| 67 # don't add a doc string, it's part of the test | |
| 68 def xmlrpc_fail(self): | |
| 69 raise TestRuntimeError | |
| 70 | |
| 71 def xmlrpc_fault(self): | |
| 72 return xmlrpc.Fault(12, "hello") | |
| 73 | |
| 74 def xmlrpc_deferFault(self): | |
| 75 return defer.fail(xmlrpc.Fault(17, "hi")) | |
| 76 | |
| 77 def xmlrpc_complex(self): | |
| 78 return {"a": ["b", "c", 12, []], "D": "foo"} | |
| 79 | |
| 80 def xmlrpc_dict(self, map, key): | |
| 81 return map[key] | |
| 82 | |
| 83 def _getFunction(self, functionPath): | |
| 84 try: | |
| 85 return XMLRPC._getFunction(self, functionPath) | |
| 86 except xmlrpc.NoSuchFunction: | |
| 87 if functionPath.startswith("SESSION"): | |
| 88 raise xmlrpc.Fault(self.SESSION_EXPIRED, | |
| 89 "Session non-existant/expired.") | |
| 90 else: | |
| 91 raise | |
| 92 | |
| 93 xmlrpc_dict.help = 'Help for dict.' | |
| 94 | |
| 95 class TestAuthHeader(Test): | |
| 96 """ | |
| 97 This is used to get the header info so that we can test | |
| 98 authentication. | |
| 99 """ | |
| 100 def __init__(self): | |
| 101 Test.__init__(self) | |
| 102 self.request = None | |
| 103 | |
| 104 def render(self, request): | |
| 105 self.request = request | |
| 106 return Test.render(self, request) | |
| 107 | |
| 108 def xmlrpc_authinfo(self): | |
| 109 return self.request.getUser(), self.request.getPassword() | |
| 110 | |
| 111 | |
| 112 class TestQueryProtocol(xmlrpc.QueryProtocol): | |
| 113 """ | |
| 114 QueryProtocol for tests that saves headers received inside the factory. | |
| 115 """ | |
| 116 def handleHeader(self, key, val): | |
| 117 self.factory.headers[key.lower()] = val | |
| 118 | |
| 119 | |
| 120 class TestQueryFactory(xmlrpc._QueryFactory): | |
| 121 """ | |
| 122 QueryFactory using L{TestQueryProtocol} for saving headers. | |
| 123 """ | |
| 124 protocol = TestQueryProtocol | |
| 125 | |
| 126 def __init__(self, *args, **kwargs): | |
| 127 self.headers = {} | |
| 128 xmlrpc._QueryFactory.__init__(self, *args, **kwargs) | |
| 129 | |
| 130 | |
| 131 class XMLRPCTestCase(unittest.TestCase): | |
| 132 | |
| 133 def setUp(self): | |
| 134 self.p = reactor.listenTCP(0, server.Site(Test()), | |
| 135 interface="127.0.0.1") | |
| 136 self.port = self.p.getHost().port | |
| 137 self.factories = [] | |
| 138 | |
| 139 def tearDown(self): | |
| 140 self.factories = [] | |
| 141 return self.p.stopListening() | |
| 142 | |
| 143 def queryFactory(self, *args, **kwargs): | |
| 144 """ | |
| 145 Specific queryFactory for proxy that uses our custom | |
| 146 L{TestQueryFactory}, and save factories. | |
| 147 """ | |
| 148 factory = TestQueryFactory(*args, **kwargs) | |
| 149 self.factories.append(factory) | |
| 150 return factory | |
| 151 | |
| 152 def proxy(self): | |
| 153 p = xmlrpc.Proxy("http://127.0.0.1:%d/" % self.port) | |
| 154 p.queryFactory = self.queryFactory | |
| 155 return p | |
| 156 | |
| 157 def test_results(self): | |
| 158 inputOutput = [ | |
| 159 ("add", (2, 3), 5), | |
| 160 ("defer", ("a",), "a"), | |
| 161 ("dict", ({"a": 1}, "a"), 1), | |
| 162 ("pair", ("a", 1), ["a", 1]), | |
| 163 ("complex", (), {"a": ["b", "c", 12, []], "D": "foo"})] | |
| 164 | |
| 165 dl = [] | |
| 166 for meth, args, outp in inputOutput: | |
| 167 d = self.proxy().callRemote(meth, *args) | |
| 168 d.addCallback(self.assertEquals, outp) | |
| 169 dl.append(d) | |
| 170 return defer.DeferredList(dl, fireOnOneErrback=True) | |
| 171 | |
| 172 def test_errors(self): | |
| 173 """ | |
| 174 Verify that for each way a method exposed via XML-RPC can fail, the | |
| 175 correct 'Content-type' header is set in the response and that the | |
| 176 client-side Deferred is errbacked with an appropriate C{Fault} | |
| 177 instance. | |
| 178 """ | |
| 179 dl = [] | |
| 180 for code, methodName in [(666, "fail"), (666, "deferFail"), | |
| 181 (12, "fault"), (23, "noSuchMethod"), | |
| 182 (17, "deferFault"), (42, "SESSION_TEST")]: | |
| 183 d = self.proxy().callRemote(methodName) | |
| 184 d = self.assertFailure(d, xmlrpc.Fault) | |
| 185 d.addCallback(lambda exc, code=code: | |
| 186 self.assertEquals(exc.faultCode, code)) | |
| 187 dl.append(d) | |
| 188 d = defer.DeferredList(dl, fireOnOneErrback=True) | |
| 189 def cb(ign): | |
| 190 for factory in self.factories: | |
| 191 self.assertEquals(factory.headers['content-type'], | |
| 192 'text/xml') | |
| 193 self.flushLoggedErrors(TestRuntimeError, TestValueError) | |
| 194 d.addCallback(cb) | |
| 195 return d | |
| 196 | |
| 197 def test_errorGet(self): | |
| 198 """ | |
| 199 A classic GET on the xml server should return a NOT_ALLOWED. | |
| 200 """ | |
| 201 d = client.getPage("http://127.0.0.1:%d/" % (self.port,)) | |
| 202 d = self.assertFailure(d, error.Error) | |
| 203 d.addCallback( | |
| 204 lambda exc: self.assertEquals(int(exc.args[0]), http.NOT_ALLOWED)) | |
| 205 return d | |
| 206 | |
| 207 def test_errorXMLContent(self): | |
| 208 """ | |
| 209 Test that an invalid XML input returns an L{xmlrpc.Fault}. | |
| 210 """ | |
| 211 d = client.getPage("http://127.0.0.1:%d/" % (self.port,), | |
| 212 method="POST", postdata="foo") | |
| 213 def cb(result): | |
| 214 self.assertRaises(xmlrpc.Fault, xmlrpclib.loads, result) | |
| 215 d.addCallback(cb) | |
| 216 return d | |
| 217 | |
| 218 | |
| 219 class XMLRPCTestCase2(XMLRPCTestCase): | |
| 220 """ | |
| 221 Test with proxy that doesn't add a slash. | |
| 222 """ | |
| 223 | |
| 224 def proxy(self): | |
| 225 p = xmlrpc.Proxy("http://127.0.0.1:%d" % self.port) | |
| 226 p.queryFactory = self.queryFactory | |
| 227 return p | |
| 228 | |
| 229 | |
| 230 | |
| 231 class XMLRPCAllowNoneTestCase(unittest.TestCase): | |
| 232 """ | |
| 233 Test with allowNone set to True. | |
| 234 | |
| 235 These are not meant to be exhaustive serialization tests, since | |
| 236 L{xmlrpclib} does all of the actual serialization work. They are just | |
| 237 meant to exercise a few codepaths to make sure we are calling into | |
| 238 xmlrpclib correctly. | |
| 239 """ | |
| 240 | |
| 241 def setUp(self): | |
| 242 self.p = reactor.listenTCP( | |
| 243 0, server.Site(Test(allowNone=True)), interface="127.0.0.1") | |
| 244 self.port = self.p.getHost().port | |
| 245 | |
| 246 | |
| 247 def tearDown(self): | |
| 248 return self.p.stopListening() | |
| 249 | |
| 250 | |
| 251 def proxy(self): | |
| 252 return xmlrpc.Proxy("http://127.0.0.1:%d" % (self.port,), | |
| 253 allowNone=True) | |
| 254 | |
| 255 | |
| 256 def test_deferredNone(self): | |
| 257 """ | |
| 258 Test that passing a C{None} as an argument to a remote method and | |
| 259 returning a L{Deferred} which fires with C{None} properly passes | |
| 260 </nil> over the network if allowNone is set to True. | |
| 261 """ | |
| 262 d = self.proxy().callRemote('defer', None) | |
| 263 d.addCallback(self.assertEquals, None) | |
| 264 return d | |
| 265 | |
| 266 | |
| 267 def test_dictWithNoneValue(self): | |
| 268 """ | |
| 269 Test that return a C{dict} with C{None} as a value works properly. | |
| 270 """ | |
| 271 d = self.proxy().callRemote('defer', {'a': None}) | |
| 272 d.addCallback(self.assertEquals, {'a': None}) | |
| 273 return d | |
| 274 | |
| 275 | |
| 276 | |
| 277 class XMLRPCTestAuthenticated(XMLRPCTestCase): | |
| 278 """ | |
| 279 Test with authenticated proxy. We run this with the same inout/ouput as | |
| 280 above. | |
| 281 """ | |
| 282 user = "username" | |
| 283 password = "asecret" | |
| 284 | |
| 285 def setUp(self): | |
| 286 self.p = reactor.listenTCP(0, server.Site(TestAuthHeader()), | |
| 287 interface="127.0.0.1") | |
| 288 self.port = self.p.getHost().port | |
| 289 self.factories = [] | |
| 290 | |
| 291 | |
| 292 def test_authInfoInURL(self): | |
| 293 p = xmlrpc.Proxy("http://%s:%s@127.0.0.1:%d/" % ( | |
| 294 self.user, self.password, self.port)) | |
| 295 d = p.callRemote("authinfo") | |
| 296 d.addCallback(self.assertEquals, [self.user, self.password]) | |
| 297 return d | |
| 298 | |
| 299 | |
| 300 def test_explicitAuthInfo(self): | |
| 301 p = xmlrpc.Proxy("http://127.0.0.1:%d/" % ( | |
| 302 self.port,), self.user, self.password) | |
| 303 d = p.callRemote("authinfo") | |
| 304 d.addCallback(self.assertEquals, [self.user, self.password]) | |
| 305 return d | |
| 306 | |
| 307 | |
| 308 def test_explicitAuthInfoOverride(self): | |
| 309 p = xmlrpc.Proxy("http://wrong:info@127.0.0.1:%d/" % ( | |
| 310 self.port,), self.user, self.password) | |
| 311 d = p.callRemote("authinfo") | |
| 312 d.addCallback(self.assertEquals, [self.user, self.password]) | |
| 313 return d | |
| 314 | |
| 315 | |
| 316 class XMLRPCTestIntrospection(XMLRPCTestCase): | |
| 317 | |
| 318 def setUp(self): | |
| 319 xmlrpc = Test() | |
| 320 addIntrospection(xmlrpc) | |
| 321 self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1") | |
| 322 self.port = self.p.getHost().port | |
| 323 self.factories = [] | |
| 324 | |
| 325 def test_listMethods(self): | |
| 326 | |
| 327 def cbMethods(meths): | |
| 328 meths.sort() | |
| 329 self.failUnlessEqual( | |
| 330 meths, | |
| 331 ['add', 'complex', 'defer', 'deferFail', | |
| 332 'deferFault', 'dict', 'fail', 'fault', | |
| 333 'pair', 'system.listMethods', | |
| 334 'system.methodHelp', | |
| 335 'system.methodSignature']) | |
| 336 | |
| 337 d = self.proxy().callRemote("system.listMethods") | |
| 338 d.addCallback(cbMethods) | |
| 339 return d | |
| 340 | |
| 341 def test_methodHelp(self): | |
| 342 inputOutputs = [ | |
| 343 ("defer", "Help for defer."), | |
| 344 ("fail", ""), | |
| 345 ("dict", "Help for dict.")] | |
| 346 | |
| 347 dl = [] | |
| 348 for meth, expected in inputOutputs: | |
| 349 d = self.proxy().callRemote("system.methodHelp", meth) | |
| 350 d.addCallback(self.assertEquals, expected) | |
| 351 dl.append(d) | |
| 352 return defer.DeferredList(dl, fireOnOneErrback=True) | |
| 353 | |
| 354 def test_methodSignature(self): | |
| 355 inputOutputs = [ | |
| 356 ("defer", ""), | |
| 357 ("add", [['int', 'int', 'int'], | |
| 358 ['double', 'double', 'double']]), | |
| 359 ("pair", [['array', 'string', 'int']])] | |
| 360 | |
| 361 dl = [] | |
| 362 for meth, expected in inputOutputs: | |
| 363 d = self.proxy().callRemote("system.methodSignature", meth) | |
| 364 d.addCallback(self.assertEquals, expected) | |
| 365 dl.append(d) | |
| 366 return defer.DeferredList(dl, fireOnOneErrback=True) | |
| 367 | |
| 368 | |
| 369 class XMLRPCClientErrorHandling(unittest.TestCase): | |
| 370 """ | |
| 371 Test error handling on the xmlrpc client. | |
| 372 """ | |
| 373 def setUp(self): | |
| 374 self.resource = static.File(__file__) | |
| 375 self.resource.isLeaf = True | |
| 376 self.port = reactor.listenTCP(0, server.Site(self.resource), | |
| 377 interface='127.0.0.1') | |
| 378 | |
| 379 def tearDown(self): | |
| 380 return self.port.stopListening() | |
| 381 | |
| 382 def test_erroneousResponse(self): | |
| 383 """ | |
| 384 Test that calling the xmlrpc client on a static http server raises | |
| 385 an exception. | |
| 386 """ | |
| 387 proxy = xmlrpc.Proxy("http://127.0.0.1:%d/" % | |
| 388 (self.port.getHost().port,)) | |
| 389 return self.assertFailure(proxy.callRemote("someMethod"), Exception) | |
| 390 | |
| 391 | |
| 392 | |
| 393 class TestQueryFactoryParseResponse(unittest.TestCase): | |
| 394 """ | |
| 395 Test the behaviour of L{_QueryFactory.parseResponse}. | |
| 396 """ | |
| 397 | |
| 398 def setUp(self): | |
| 399 # The _QueryFactory that we are testing. We don't care about any | |
| 400 # of the constructor parameters. | |
| 401 self.queryFactory = _QueryFactory( | |
| 402 path=None, host=None, method='POST', user=None, password=None, | |
| 403 allowNone=False, args=()) | |
| 404 # An XML-RPC response that will parse without raising an error. | |
| 405 self.goodContents = xmlrpclib.dumps(('',)) | |
| 406 # An 'XML-RPC response' that will raise a parsing error. | |
| 407 self.badContents = 'invalid xml' | |
| 408 # A dummy 'reason' to pass to clientConnectionLost. We don't care | |
| 409 # what it is. | |
| 410 self.reason = failure.Failure(ConnectionDone()) | |
| 411 | |
| 412 | |
| 413 def test_parseResponseCallbackSafety(self): | |
| 414 """ | |
| 415 We can safely call L{_QueryFactory.clientConnectionLost} as a callback | |
| 416 of L{_QueryFactory.parseResponse}. | |
| 417 """ | |
| 418 d = self.queryFactory.deferred | |
| 419 # The failure mode is that this callback raises an AlreadyCalled | |
| 420 # error. We have to add it now so that it gets called synchronously | |
| 421 # and triggers the race condition. | |
| 422 d.addCallback(self.queryFactory.clientConnectionLost, self.reason) | |
| 423 self.queryFactory.parseResponse(self.goodContents) | |
| 424 return d | |
| 425 | |
| 426 | |
| 427 def test_parseResponseErrbackSafety(self): | |
| 428 """ | |
| 429 We can safely call L{_QueryFactory.clientConnectionLost} as an errback | |
| 430 of L{_QueryFactory.parseResponse}. | |
| 431 """ | |
| 432 d = self.queryFactory.deferred | |
| 433 # The failure mode is that this callback raises an AlreadyCalled | |
| 434 # error. We have to add it now so that it gets called synchronously | |
| 435 # and triggers the race condition. | |
| 436 d.addErrback(self.queryFactory.clientConnectionLost, self.reason) | |
| 437 self.queryFactory.parseResponse(self.badContents) | |
| 438 return d | |
| 439 | |
| 440 | |
| 441 def test_badStatusErrbackSafety(self): | |
| 442 """ | |
| 443 We can safely call L{_QueryFactory.clientConnectionLost} as an errback | |
| 444 of L{_QueryFactory.badStatus}. | |
| 445 """ | |
| 446 d = self.queryFactory.deferred | |
| 447 # The failure mode is that this callback raises an AlreadyCalled | |
| 448 # error. We have to add it now so that it gets called synchronously | |
| 449 # and triggers the race condition. | |
| 450 d.addErrback(self.queryFactory.clientConnectionLost, self.reason) | |
| 451 self.queryFactory.badStatus('status', 'message') | |
| 452 return d | |
| OLD | NEW |