1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for L{twisted.web.distrib}.
8 from os.path import abspath
9 from xml.dom.minidom import parseString
15 from zope.interface.verify import verifyObject
17 from twisted.python import log, filepath
18 from twisted.internet import reactor, defer
19 from twisted.trial import unittest
20 from twisted.spread import pb
21 from twisted.spread.banana import SIZE_LIMIT
22 from twisted.web import http, distrib, client, resource, static, server
23 from twisted.web.test.test_web import DummyRequest
24 from twisted.web.test._util import _render
25 from twisted.test import proto_helpers
28 class MySite(server.Site):
32 class PBServerFactory(pb.PBServerFactory):
34 A PB server factory which keeps track of the most recent protocol it
37 @ivar proto: L{None} or the L{Broker} instance most recently returned
38 from C{buildProtocol}.
42 def buildProtocol(self, addr):
43 self.proto = pb.PBServerFactory.buildProtocol(self, addr)
48 class DistribTest(unittest.TestCase):
56 Clean up all the event sources left behind by either directly by
57 test methods or indirectly via some distrib API.
59 dl = [defer.Deferred(), defer.Deferred()]
60 if self.f1 is not None and self.f1.proto is not None:
61 self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
64 if self.sub is not None and self.sub.publisher is not None:
65 self.sub.publisher.broker.notifyOnDisconnect(
66 lambda: dl[1].callback(None))
67 self.sub.publisher.broker.transport.loseConnection()
70 if self.port1 is not None:
71 dl.append(self.port1.stopListening())
72 if self.port2 is not None:
73 dl.append(self.port2.stopListening())
74 return defer.gatherResults(dl)
77 def testDistrib(self):
78 # site1 is the publisher
79 r1 = resource.Resource()
80 r1.putChild("there", static.Data("root", "text/plain"))
81 site1 = server.Site(r1)
82 self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
83 self.port1 = reactor.listenTCP(0, self.f1)
84 self.sub = distrib.ResourceSubscription("127.0.0.1",
85 self.port1.getHost().port)
86 r2 = resource.Resource()
87 r2.putChild("here", self.sub)
89 self.port2 = reactor.listenTCP(0, f2)
90 d = client.getPage("http://127.0.0.1:%d/here/there" % \
91 self.port2.getHost().port)
92 d.addCallback(self.assertEqual, 'root')
96 def _setupDistribServer(self, child):
98 Set up a resource on a distrib site using L{ResourcePublisher}.
100 @param child: The resource to publish using distrib.
102 @return: A tuple consisting of the host and port on which to contact
105 distribRoot = resource.Resource()
106 distribRoot.putChild("child", child)
107 distribSite = server.Site(distribRoot)
108 self.f1 = distribFactory = PBServerFactory(
109 distrib.ResourcePublisher(distribSite))
110 distribPort = reactor.listenTCP(
111 0, distribFactory, interface="127.0.0.1")
112 self.addCleanup(distribPort.stopListening)
113 addr = distribPort.getHost()
115 self.sub = mainRoot = distrib.ResourceSubscription(
116 addr.host, addr.port)
117 mainSite = server.Site(mainRoot)
118 mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
119 self.addCleanup(mainPort.stopListening)
120 mainAddr = mainPort.getHost()
122 return mainPort, mainAddr
125 def _requestTest(self, child, **kwargs):
127 Set up a resource on a distrib site using L{ResourcePublisher} and
128 then retrieve it from a L{ResourceSubscription} via an HTTP client.
130 @param child: The resource to publish using distrib.
131 @param **kwargs: Extra keyword arguments to pass to L{getPage} when
132 requesting the resource.
134 @return: A L{Deferred} which fires with the result of the request.
136 mainPort, mainAddr = self._setupDistribServer(child)
137 return client.getPage("http://%s:%s/child" % (
138 mainAddr.host, mainAddr.port), **kwargs)
141 def _requestAgentTest(self, child, **kwargs):
143 Set up a resource on a distrib site using L{ResourcePublisher} and
144 then retrieve it from a L{ResourceSubscription} via an HTTP client.
146 @param child: The resource to publish using distrib.
147 @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
148 requesting the resource.
150 @return: A L{Deferred} which fires with a tuple consisting of a
151 L{twisted.test.proto_helpers.AccumulatingProtocol} containing the
152 body of the response and an L{IResponse} with the response itself.
154 mainPort, mainAddr = self._setupDistribServer(child)
156 d = client.Agent(reactor).request("GET", "http://%s:%s/child" % (
157 mainAddr.host, mainAddr.port), **kwargs)
159 def cbCollectBody(response):
160 protocol = proto_helpers.AccumulatingProtocol()
161 response.deliverBody(protocol)
162 d = protocol.closedDeferred = defer.Deferred()
163 d.addCallback(lambda _: (protocol, response))
165 d.addCallback(cbCollectBody)
169 def test_requestHeaders(self):
171 The request headers are available on the request object passed to a
172 distributed resource's C{render} method.
176 class ReportRequestHeaders(resource.Resource):
177 def render(self, request):
178 requestHeaders.update(dict(
179 request.requestHeaders.getAllRawHeaders()))
182 request = self._requestTest(
183 ReportRequestHeaders(), headers={'foo': 'bar'})
184 def cbRequested(result):
185 self.assertEqual(requestHeaders['Foo'], ['bar'])
186 request.addCallback(cbRequested)
190 def test_requestResponseCode(self):
192 The response code can be set by the request object passed to a
193 distributed resource's C{render} method.
195 class SetResponseCode(resource.Resource):
196 def render(self, request):
197 request.setResponseCode(200)
200 request = self._requestAgentTest(SetResponseCode())
201 def cbRequested(result):
202 self.assertEqual(result[0].data, "")
203 self.assertEqual(result[1].code, 200)
204 self.assertEqual(result[1].phrase, "OK")
205 request.addCallback(cbRequested)
209 def test_requestResponseCodeMessage(self):
211 The response code and message can be set by the request object passed to
212 a distributed resource's C{render} method.
214 class SetResponseCode(resource.Resource):
215 def render(self, request):
216 request.setResponseCode(200, "some-message")
219 request = self._requestAgentTest(SetResponseCode())
220 def cbRequested(result):
221 self.assertEqual(result[0].data, "")
222 self.assertEqual(result[1].code, 200)
223 self.assertEqual(result[1].phrase, "some-message")
224 request.addCallback(cbRequested)
228 def test_largeWrite(self):
230 If a string longer than the Banana size limit is passed to the
231 L{distrib.Request} passed to the remote resource, it is broken into
232 smaller strings to be transported over the PB connection.
234 class LargeWrite(resource.Resource):
235 def render(self, request):
236 request.write('x' * SIZE_LIMIT + 'y')
238 return server.NOT_DONE_YET
240 request = self._requestTest(LargeWrite())
241 request.addCallback(self.assertEqual, 'x' * SIZE_LIMIT + 'y')
245 def test_largeReturn(self):
247 Like L{test_largeWrite}, but for the case where C{render} returns a
248 long string rather than explicitly passing it to L{Request.write}.
250 class LargeReturn(resource.Resource):
251 def render(self, request):
252 return 'x' * SIZE_LIMIT + 'y'
254 request = self._requestTest(LargeReturn())
255 request.addCallback(self.assertEqual, 'x' * SIZE_LIMIT + 'y')
259 def test_connectionLost(self):
261 If there is an error issuing the request to the remote publisher, an
262 error response is returned.
264 # Using pb.Root as a publisher will cause request calls to fail with an
265 # error every time. Just what we want to test.
266 self.f1 = serverFactory = PBServerFactory(pb.Root())
267 self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
269 self.sub = subscription = distrib.ResourceSubscription(
270 "127.0.0.1", serverPort.getHost().port)
271 request = DummyRequest([''])
272 d = _render(subscription, request)
273 def cbRendered(ignored):
274 self.assertEqual(request.responseCode, 500)
275 # This is the error we caused the request to fail with. It should
277 self.assertEqual(len(self.flushLoggedErrors(pb.NoSuchMethod)), 1)
278 d.addCallback(cbRendered)
283 class _PasswordDatabase:
284 def __init__(self, users):
289 return iter(self._users)
292 def getpwnam(self, username):
293 for user in self._users:
294 if user[0] == username:
300 class UserDirectoryTests(unittest.TestCase):
302 Tests for L{UserDirectory}, a resource for listing all user resources
303 available on a system.
306 self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
307 self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
308 self.database = _PasswordDatabase([self.alice, self.bob])
309 self.directory = distrib.UserDirectory(self.database)
312 def test_interface(self):
314 L{UserDirectory} instances provide L{resource.IResource}.
316 self.assertTrue(verifyObject(resource.IResource, self.directory))
319 def _404Test(self, name):
321 Verify that requesting the C{name} child of C{self.directory} results
324 request = DummyRequest([name])
325 result = self.directory.getChild(name, request)
326 d = _render(result, request)
327 def cbRendered(ignored):
328 self.assertEqual(request.responseCode, 404)
329 d.addCallback(cbRendered)
333 def test_getInvalidUser(self):
335 L{UserDirectory.getChild} returns a resource which renders a 404
336 response when passed a string which does not correspond to any known
339 return self._404Test('carol')
342 def test_getUserWithoutResource(self):
344 L{UserDirectory.getChild} returns a resource which renders a 404
345 response when passed a string which corresponds to a known user who has
346 neither a user directory nor a user distrib socket.
348 return self._404Test('alice')
351 def test_getPublicHTMLChild(self):
353 L{UserDirectory.getChild} returns a L{static.File} instance when passed
354 the name of a user with a home directory containing a I{public_html}
357 home = filepath.FilePath(self.bob[-2])
358 public_html = home.child('public_html')
359 public_html.makedirs()
360 request = DummyRequest(['bob'])
361 result = self.directory.getChild('bob', request)
362 self.assertIsInstance(result, static.File)
363 self.assertEqual(result.path, public_html.path)
366 def test_getDistribChild(self):
368 L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
369 when passed the name of a user suffixed with C{".twistd"} who has a
370 home directory containing a I{.twistd-web-pb} socket.
372 home = filepath.FilePath(self.bob[-2])
374 web = home.child('.twistd-web-pb')
375 request = DummyRequest(['bob'])
376 result = self.directory.getChild('bob.twistd', request)
377 self.assertIsInstance(result, distrib.ResourceSubscription)
378 self.assertEqual(result.host, 'unix')
379 self.assertEqual(abspath(result.port), web.path)
382 def test_invalidMethod(self):
384 L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
387 request = DummyRequest([''])
388 request.method = 'POST'
390 server.UnsupportedMethod, self.directory.render, request)
393 def test_render(self):
395 L{UserDirectory} renders a list of links to available user content
396 in response to a I{GET} request.
398 public_html = filepath.FilePath(self.alice[-2]).child('public_html')
399 public_html.makedirs()
400 web = filepath.FilePath(self.bob[-2])
402 # This really only works if it's a unix socket, but the implementation
403 # doesn't currently check for that. It probably should someday, and
404 # then skip users with non-sockets.
405 web.child('.twistd-web-pb').setContent("")
407 request = DummyRequest([''])
408 result = _render(self.directory, request)
409 def cbRendered(ignored):
410 document = parseString(''.join(request.written))
412 # Each user should have an li with a link to their page.
413 [alice, bob] = document.getElementsByTagName('li')
414 self.assertEqual(alice.firstChild.tagName, 'a')
415 self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
416 self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
417 self.assertEqual(bob.firstChild.tagName, 'a')
418 self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
419 self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
421 result.addCallback(cbRendered)
425 def test_passwordDatabase(self):
427 If L{UserDirectory} is instantiated with no arguments, it uses the
428 L{pwd} module as its password database.
430 directory = distrib.UserDirectory()
431 self.assertIdentical(directory._pwd, pwd)
433 test_passwordDatabase.skip = "pwd module required"