1
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
2
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Tests for XML-RPC support in L{twisted.web.xmlrpc}.
11
from twisted.trial import unittest
12
from twisted.web import xmlrpc
13
from twisted.web.xmlrpc import XMLRPC, addIntrospection, _QueryFactory
14
from twisted.web import server, static, client, error, http
15
from twisted.internet import reactor, defer
16
from twisted.internet.error import ConnectionDone
17
from twisted.python import failure
20
class TestRuntimeError(RuntimeError):
23
class TestValueError(ValueError):
30
# If you add xmlrpc_ methods to this class, go change test_listMethods
37
def xmlrpc_echo(self, arg):
40
# the doc string is part of the test
41
def xmlrpc_add(self, a, b):
43
This function add two numbers.
47
xmlrpc_add.signature = [['int', 'int', 'int'],
48
['double', 'double', 'double']]
50
# the doc string is part of the test
51
def xmlrpc_pair(self, string, num):
53
This function puts the two arguments in an array.
57
xmlrpc_pair.signature = [['array', 'string', 'int']]
59
# the doc string is part of the test
60
def xmlrpc_defer(self, x):
62
return defer.succeed(x)
64
def xmlrpc_deferFail(self):
65
return defer.fail(TestValueError())
67
# don't add a doc string, it's part of the test
68
def xmlrpc_fail(self):
69
raise TestRuntimeError
71
def xmlrpc_fault(self):
72
return xmlrpc.Fault(12, "hello")
74
def xmlrpc_deferFault(self):
75
return defer.fail(xmlrpc.Fault(17, "hi"))
77
def xmlrpc_complex(self):
78
return {"a": ["b", "c", 12, []], "D": "foo"}
80
def xmlrpc_dict(self, map, key):
82
xmlrpc_dict.help = 'Help for dict.'
84
def _getFunction(self, functionPath):
86
return XMLRPC._getFunction(self, functionPath)
87
except xmlrpc.NoSuchFunction:
88
if functionPath.startswith("SESSION"):
89
raise xmlrpc.Fault(self.SESSION_EXPIRED,
90
"Session non-existant/expired.")
95
class TestAuthHeader(Test):
97
This is used to get the header info so that we can test
104
def render(self, request):
105
self.request = request
106
return Test.render(self, request)
108
def xmlrpc_authinfo(self):
109
return self.request.getUser(), self.request.getPassword()
112
class TestQueryProtocol(xmlrpc.QueryProtocol):
114
QueryProtocol for tests that saves headers received inside the factory.
116
def handleHeader(self, key, val):
117
self.factory.headers[key.lower()] = val
120
class TestQueryFactory(xmlrpc._QueryFactory):
122
QueryFactory using L{TestQueryProtocol} for saving headers.
124
protocol = TestQueryProtocol
126
def __init__(self, *args, **kwargs):
128
xmlrpc._QueryFactory.__init__(self, *args, **kwargs)
131
class XMLRPCTestCase(unittest.TestCase):
134
self.p = reactor.listenTCP(0, server.Site(Test()),
135
interface="127.0.0.1")
136
self.port = self.p.getHost().port
141
return self.p.stopListening()
143
def queryFactory(self, *args, **kwargs):
145
Specific queryFactory for proxy that uses our custom
146
L{TestQueryFactory}, and save factories.
148
factory = TestQueryFactory(*args, **kwargs)
149
self.factories.append(factory)
153
p = xmlrpc.Proxy("http://127.0.0.1:%d/" % self.port)
154
p.queryFactory = self.queryFactory
157
def test_results(self):
160
("defer", ("a",), "a"),
161
("dict", ({"a": 1}, "a"), 1),
162
("pair", ("a", 1), ["a", 1]),
163
("complex", (), {"a": ["b", "c", 12, []], "D": "foo"})]
166
for meth, args, outp in inputOutput:
167
d = self.proxy().callRemote(meth, *args)
168
d.addCallback(self.assertEquals, outp)
170
return defer.DeferredList(dl, fireOnOneErrback=True)
172
def test_errors(self):
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}
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))
188
d = defer.DeferredList(dl, fireOnOneErrback=True)
190
for factory in self.factories:
191
self.assertEquals(factory.headers['content-type'],
193
self.flushLoggedErrors(TestRuntimeError, TestValueError)
197
def test_errorGet(self):
199
A classic GET on the xml server should return a NOT_ALLOWED.
201
d = client.getPage("http://127.0.0.1:%d/" % (self.port,))
202
d = self.assertFailure(d, error.Error)
204
lambda exc: self.assertEquals(int(exc.args[0]), http.NOT_ALLOWED))
207
def test_errorXMLContent(self):
209
Test that an invalid XML input returns an L{xmlrpc.Fault}.
211
d = client.getPage("http://127.0.0.1:%d/" % (self.port,),
212
method="POST", postdata="foo")
214
self.assertRaises(xmlrpc.Fault, xmlrpclib.loads, result)
219
def test_datetimeRoundtrip(self):
221
If an L{xmlrpclib.DateTime} is passed as an argument to an XML-RPC
222
call and then returned by the server unmodified, the result should
223
be equal to the original object.
225
when = xmlrpclib.DateTime()
226
d = self.proxy().callRemote("echo", when)
227
d.addCallback(self.assertEqual, when)
231
def test_doubleEncodingError(self):
233
If it is not possible to encode a response to the request (for example,
234
because L{xmlrpclib.dumps} raises an exception when encoding a
235
L{Fault}) the exception which prevents the response from being
236
generated is logged and the request object is finished anyway.
238
d = self.proxy().callRemote("echo", "")
240
# *Now* break xmlrpclib.dumps. Hopefully the client already used it.
241
def fakeDumps(*args, **kwargs):
242
raise RuntimeError("Cannot encode anything at all!")
243
self.patch(xmlrpclib, 'dumps', fakeDumps)
245
# It doesn't matter how it fails, so long as it does. Also, it happens
246
# to fail with an implementation detail exception right now, not
247
# something suitable as part of a public interface.
248
d = self.assertFailure(d, Exception)
250
def cbFailed(ignored):
251
# The fakeDumps exception should have been logged.
252
self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
253
d.addCallback(cbFailed)
258
class XMLRPCTestCase2(XMLRPCTestCase):
260
Test with proxy that doesn't add a slash.
264
p = xmlrpc.Proxy("http://127.0.0.1:%d" % self.port)
265
p.queryFactory = self.queryFactory
270
class XMLRPCAllowNoneTestCase(unittest.TestCase):
272
Test with allowNone set to True.
274
These are not meant to be exhaustive serialization tests, since
275
L{xmlrpclib} does all of the actual serialization work. They are just
276
meant to exercise a few codepaths to make sure we are calling into
281
self.p = reactor.listenTCP(
282
0, server.Site(Test(allowNone=True)), interface="127.0.0.1")
283
self.port = self.p.getHost().port
287
return self.p.stopListening()
291
return xmlrpc.Proxy("http://127.0.0.1:%d" % (self.port,),
295
def test_deferredNone(self):
297
Test that passing a C{None} as an argument to a remote method and
298
returning a L{Deferred} which fires with C{None} properly passes
299
</nil> over the network if allowNone is set to True.
301
d = self.proxy().callRemote('defer', None)
302
d.addCallback(self.assertEquals, None)
306
def test_dictWithNoneValue(self):
308
Test that return a C{dict} with C{None} as a value works properly.
310
d = self.proxy().callRemote('defer', {'a': None})
311
d.addCallback(self.assertEquals, {'a': None})
316
class XMLRPCTestAuthenticated(XMLRPCTestCase):
318
Test with authenticated proxy. We run this with the same inout/ouput as
325
self.p = reactor.listenTCP(0, server.Site(TestAuthHeader()),
326
interface="127.0.0.1")
327
self.port = self.p.getHost().port
331
def test_authInfoInURL(self):
332
p = xmlrpc.Proxy("http://%s:%s@127.0.0.1:%d/" % (
333
self.user, self.password, self.port))
334
d = p.callRemote("authinfo")
335
d.addCallback(self.assertEquals, [self.user, self.password])
339
def test_explicitAuthInfo(self):
340
p = xmlrpc.Proxy("http://127.0.0.1:%d/" % (
341
self.port,), self.user, self.password)
342
d = p.callRemote("authinfo")
343
d.addCallback(self.assertEquals, [self.user, self.password])
347
def test_explicitAuthInfoOverride(self):
348
p = xmlrpc.Proxy("http://wrong:info@127.0.0.1:%d/" % (
349
self.port,), self.user, self.password)
350
d = p.callRemote("authinfo")
351
d.addCallback(self.assertEquals, [self.user, self.password])
355
class XMLRPCTestIntrospection(XMLRPCTestCase):
359
addIntrospection(xmlrpc)
360
self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1")
361
self.port = self.p.getHost().port
364
def test_listMethods(self):
366
def cbMethods(meths):
370
['add', 'complex', 'defer', 'deferFail',
371
'deferFault', 'dict', 'echo', 'fail', 'fault',
372
'pair', 'system.listMethods',
374
'system.methodSignature'])
376
d = self.proxy().callRemote("system.listMethods")
377
d.addCallback(cbMethods)
380
def test_methodHelp(self):
382
("defer", "Help for defer."),
384
("dict", "Help for dict.")]
387
for meth, expected in inputOutputs:
388
d = self.proxy().callRemote("system.methodHelp", meth)
389
d.addCallback(self.assertEquals, expected)
391
return defer.DeferredList(dl, fireOnOneErrback=True)
393
def test_methodSignature(self):
396
("add", [['int', 'int', 'int'],
397
['double', 'double', 'double']]),
398
("pair", [['array', 'string', 'int']])]
401
for meth, expected in inputOutputs:
402
d = self.proxy().callRemote("system.methodSignature", meth)
403
d.addCallback(self.assertEquals, expected)
405
return defer.DeferredList(dl, fireOnOneErrback=True)
408
class XMLRPCClientErrorHandling(unittest.TestCase):
410
Test error handling on the xmlrpc client.
413
self.resource = static.Data(
414
"This text is not a valid XML-RPC response.",
416
self.resource.isLeaf = True
417
self.port = reactor.listenTCP(0, server.Site(self.resource),
418
interface='127.0.0.1')
421
return self.port.stopListening()
423
def test_erroneousResponse(self):
425
Test that calling the xmlrpc client on a static http server raises
428
proxy = xmlrpc.Proxy("http://127.0.0.1:%d/" %
429
(self.port.getHost().port,))
430
return self.assertFailure(proxy.callRemote("someMethod"), Exception)
434
class TestQueryFactoryParseResponse(unittest.TestCase):
436
Test the behaviour of L{_QueryFactory.parseResponse}.
440
# The _QueryFactory that we are testing. We don't care about any
441
# of the constructor parameters.
442
self.queryFactory = _QueryFactory(
443
path=None, host=None, method='POST', user=None, password=None,
444
allowNone=False, args=())
445
# An XML-RPC response that will parse without raising an error.
446
self.goodContents = xmlrpclib.dumps(('',))
447
# An 'XML-RPC response' that will raise a parsing error.
448
self.badContents = 'invalid xml'
449
# A dummy 'reason' to pass to clientConnectionLost. We don't care
451
self.reason = failure.Failure(ConnectionDone())
454
def test_parseResponseCallbackSafety(self):
456
We can safely call L{_QueryFactory.clientConnectionLost} as a callback
457
of L{_QueryFactory.parseResponse}.
459
d = self.queryFactory.deferred
460
# The failure mode is that this callback raises an AlreadyCalled
461
# error. We have to add it now so that it gets called synchronously
462
# and triggers the race condition.
463
d.addCallback(self.queryFactory.clientConnectionLost, self.reason)
464
self.queryFactory.parseResponse(self.goodContents)
468
def test_parseResponseErrbackSafety(self):
470
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
471
of L{_QueryFactory.parseResponse}.
473
d = self.queryFactory.deferred
474
# The failure mode is that this callback raises an AlreadyCalled
475
# error. We have to add it now so that it gets called synchronously
476
# and triggers the race condition.
477
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
478
self.queryFactory.parseResponse(self.badContents)
482
def test_badStatusErrbackSafety(self):
484
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
485
of L{_QueryFactory.badStatus}.
487
d = self.queryFactory.deferred
488
# The failure mode is that this callback raises an AlreadyCalled
489
# error. We have to add it now so that it gets called synchronously
490
# and triggers the race condition.
491
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
492
self.queryFactory.badStatus('status', 'message')
495
def test_parseResponseWithoutData(self):
497
Some server can send a response without any data:
498
L{_QueryFactory.parseResponse} should catch the error and call the
508
d = self.queryFactory.deferred
509
self.queryFactory.parseResponse(content)
510
return self.assertFailure(d, IndexError)