1
# Copyright (c) 2008-2010 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
27
class MySite(server.Site):
28
def stopFactory(self):
29
if hasattr(self, "logFile"):
30
if self.logFile != log.logfile:
36
class PBServerFactory(pb.PBServerFactory):
38
A PB server factory which keeps track of the most recent protocol it
41
@ivar proto: L{None} or the L{Broker} instance most recently returned
42
from C{buildProtocol}.
46
def buildProtocol(self, addr):
47
self.proto = pb.PBServerFactory.buildProtocol(self, addr)
52
class DistribTest(unittest.TestCase):
60
Clean up all the event sources left behind by either directly by
61
test methods or indirectly via some distrib API.
63
dl = [defer.Deferred(), defer.Deferred()]
64
if self.f1 is not None and self.f1.proto is not None:
65
self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
68
if self.sub is not None and self.sub.publisher is not None:
69
self.sub.publisher.broker.notifyOnDisconnect(
70
lambda: dl[1].callback(None))
71
self.sub.publisher.broker.transport.loseConnection()
74
http._logDateTimeStop()
75
if self.port1 is not None:
76
dl.append(self.port1.stopListening())
77
if self.port2 is not None:
78
dl.append(self.port2.stopListening())
79
return defer.gatherResults(dl)
82
def testDistrib(self):
83
# site1 is the publisher
84
r1 = resource.Resource()
85
r1.putChild("there", static.Data("root", "text/plain"))
86
site1 = server.Site(r1)
87
self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
88
self.port1 = reactor.listenTCP(0, self.f1)
89
self.sub = distrib.ResourceSubscription("127.0.0.1",
90
self.port1.getHost().port)
91
r2 = resource.Resource()
92
r2.putChild("here", self.sub)
94
self.port2 = reactor.listenTCP(0, f2)
95
d = client.getPage("http://127.0.0.1:%d/here/there" % \
96
self.port2.getHost().port)
97
d.addCallback(self.failUnlessEqual, 'root')
101
def _requestTest(self, child, **kwargs):
103
Set up a resource on a distrib site using L{ResourcePublisher} and
104
then retrieve it from a L{ResourceSubscription} via an HTTP client.
106
@param child: The resource to publish using distrib.
107
@param **kwargs: Extra keyword arguments to pass to L{getPage} when
108
requesting the resource.
110
@return: A L{Deferred} which fires with the result of the request.
112
distribRoot = resource.Resource()
113
distribRoot.putChild("child", child)
114
distribSite = server.Site(distribRoot)
115
self.f1 = distribFactory = PBServerFactory(
116
distrib.ResourcePublisher(distribSite))
117
distribPort = reactor.listenTCP(
118
0, distribFactory, interface="127.0.0.1")
119
self.addCleanup(distribPort.stopListening)
120
addr = distribPort.getHost()
122
self.sub = mainRoot = distrib.ResourceSubscription(
123
addr.host, addr.port)
124
mainSite = server.Site(mainRoot)
125
mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
126
self.addCleanup(mainPort.stopListening)
127
mainAddr = mainPort.getHost()
129
return client.getPage("http://%s:%s/child" % (
130
mainAddr.host, mainAddr.port), **kwargs)
134
def test_requestHeaders(self):
136
The request headers are available on the request object passed to a
137
distributed resource's C{render} method.
141
class ReportRequestHeaders(resource.Resource):
142
def render(self, request):
143
requestHeaders.update(dict(
144
request.requestHeaders.getAllRawHeaders()))
147
request = self._requestTest(
148
ReportRequestHeaders(), headers={'foo': 'bar'})
149
def cbRequested(result):
150
self.assertEquals(requestHeaders['Foo'], ['bar'])
151
request.addCallback(cbRequested)
155
def test_largeWrite(self):
157
If a string longer than the Banana size limit is passed to the
158
L{distrib.Request} passed to the remote resource, it is broken into
159
smaller strings to be transported over the PB connection.
161
class LargeWrite(resource.Resource):
162
def render(self, request):
163
request.write('x' * SIZE_LIMIT + 'y')
165
return server.NOT_DONE_YET
167
request = self._requestTest(LargeWrite())
168
request.addCallback(self.assertEquals, 'x' * SIZE_LIMIT + 'y')
172
def test_largeReturn(self):
174
Like L{test_largeWrite}, but for the case where C{render} returns a
175
long string rather than explicitly passing it to L{Request.write}.
177
class LargeReturn(resource.Resource):
178
def render(self, request):
179
return 'x' * SIZE_LIMIT + 'y'
181
request = self._requestTest(LargeReturn())
182
request.addCallback(self.assertEquals, 'x' * SIZE_LIMIT + 'y')
186
def test_connectionLost(self):
188
If there is an error issuing the request to the remote publisher, an
189
error response is returned.
191
# Using pb.Root as a publisher will cause request calls to fail with an
192
# error every time. Just what we want to test.
193
self.f1 = serverFactory = PBServerFactory(pb.Root())
194
self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
196
self.sub = subscription = distrib.ResourceSubscription(
197
"127.0.0.1", serverPort.getHost().port)
198
request = DummyRequest([''])
199
d = _render(subscription, request)
200
def cbRendered(ignored):
201
self.assertEqual(request.responseCode, 500)
202
# This is the error we caused the request to fail with. It should
204
self.assertEqual(len(self.flushLoggedErrors(pb.NoSuchMethod)), 1)
205
d.addCallback(cbRendered)
210
class _PasswordDatabase:
211
def __init__(self, users):
216
return iter(self._users)
219
def getpwnam(self, username):
220
for user in self._users:
221
if user[0] == username:
227
class UserDirectoryTests(unittest.TestCase):
229
Tests for L{UserDirectory}, a resource for listing all user resources
230
available on a system.
233
self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
234
self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
235
self.database = _PasswordDatabase([self.alice, self.bob])
236
self.directory = distrib.UserDirectory(self.database)
239
def test_interface(self):
241
L{UserDirectory} instances provide L{resource.IResource}.
243
self.assertTrue(verifyObject(resource.IResource, self.directory))
246
def _404Test(self, name):
248
Verify that requesting the C{name} child of C{self.directory} results
251
request = DummyRequest([name])
252
result = self.directory.getChild(name, request)
253
d = _render(result, request)
254
def cbRendered(ignored):
255
self.assertEqual(request.responseCode, 404)
256
d.addCallback(cbRendered)
260
def test_getInvalidUser(self):
262
L{UserDirectory.getChild} returns a resource which renders a 404
263
response when passed a string which does not correspond to any known
266
return self._404Test('carol')
269
def test_getUserWithoutResource(self):
271
L{UserDirectory.getChild} returns a resource which renders a 404
272
response when passed a string which corresponds to a known user who has
273
neither a user directory nor a user distrib socket.
275
return self._404Test('alice')
278
def test_getPublicHTMLChild(self):
280
L{UserDirectory.getChild} returns a L{static.File} instance when passed
281
the name of a user with a home directory containing a I{public_html}
284
home = filepath.FilePath(self.bob[-2])
285
public_html = home.child('public_html')
286
public_html.makedirs()
287
request = DummyRequest(['bob'])
288
result = self.directory.getChild('bob', request)
289
self.assertIsInstance(result, static.File)
290
self.assertEqual(result.path, public_html.path)
293
def test_getDistribChild(self):
295
L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
296
when passed the name of a user suffixed with C{".twistd"} who has a
297
home directory containing a I{.twistd-web-pb} socket.
299
home = filepath.FilePath(self.bob[-2])
301
web = home.child('.twistd-web-pb')
302
request = DummyRequest(['bob'])
303
result = self.directory.getChild('bob.twistd', request)
304
self.assertIsInstance(result, distrib.ResourceSubscription)
305
self.assertEqual(result.host, 'unix')
306
self.assertEqual(abspath(result.port), web.path)
309
def test_invalidMethod(self):
311
L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
314
request = DummyRequest([''])
315
request.method = 'POST'
317
server.UnsupportedMethod, self.directory.render, request)
320
def test_render(self):
322
L{UserDirectory} renders a list of links to available user content
323
in response to a I{GET} request.
325
public_html = filepath.FilePath(self.alice[-2]).child('public_html')
326
public_html.makedirs()
327
web = filepath.FilePath(self.bob[-2])
329
# This really only works if it's a unix socket, but the implementation
330
# doesn't currently check for that. It probably should someday, and
331
# then skip users with non-sockets.
332
web.child('.twistd-web-pb').setContent("")
334
request = DummyRequest([''])
335
result = _render(self.directory, request)
336
def cbRendered(ignored):
337
document = parseString(''.join(request.written))
339
# Each user should have an li with a link to their page.
340
[alice, bob] = document.getElementsByTagName('li')
341
self.assertEqual(alice.firstChild.tagName, 'a')
342
self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
343
self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
344
self.assertEqual(bob.firstChild.tagName, 'a')
345
self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
346
self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
348
result.addCallback(cbRendered)
352
def test_passwordDatabase(self):
354
If L{UserDirectory} is instantiated with no arguments, it uses the
355
L{pwd} module as its password database.
357
directory = distrib.UserDirectory()
358
self.assertIdentical(directory._pwd, pwd)
360
test_passwordDatabase.skip = "pwd module required"