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 |