123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Tests for L{twisted.web.distrib}.
- """
- from os.path import abspath
- from xml.dom.minidom import parseString
- try:
- import pwd
- except ImportError:
- pwd = None
- from zope.interface.verify import verifyObject
- from twisted.python import filepath
- from twisted.internet import reactor, defer
- from twisted.trial import unittest
- from twisted.spread import pb
- from twisted.spread.banana import SIZE_LIMIT
- from twisted.web import distrib, client, resource, static, server
- from twisted.web.test.test_web import DummyRequest
- from twisted.web.test._util import _render
- from twisted.test import proto_helpers
- from twisted.web.http_headers import Headers
- class MySite(server.Site):
- pass
- class PBServerFactory(pb.PBServerFactory):
- """
- A PB server factory which keeps track of the most recent protocol it
- created.
- @ivar proto: L{None} or the L{Broker} instance most recently returned
- from C{buildProtocol}.
- """
- proto = None
- def buildProtocol(self, addr):
- self.proto = pb.PBServerFactory.buildProtocol(self, addr)
- return self.proto
- class DistribTests(unittest.TestCase):
- port1 = None
- port2 = None
- sub = None
- f1 = None
- def tearDown(self):
- """
- Clean up all the event sources left behind by either directly by
- test methods or indirectly via some distrib API.
- """
- dl = [defer.Deferred(), defer.Deferred()]
- if self.f1 is not None and self.f1.proto is not None:
- self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
- else:
- dl[0].callback(None)
- if self.sub is not None and self.sub.publisher is not None:
- self.sub.publisher.broker.notifyOnDisconnect(
- lambda: dl[1].callback(None))
- self.sub.publisher.broker.transport.loseConnection()
- else:
- dl[1].callback(None)
- if self.port1 is not None:
- dl.append(self.port1.stopListening())
- if self.port2 is not None:
- dl.append(self.port2.stopListening())
- return defer.gatherResults(dl)
- def testDistrib(self):
- # site1 is the publisher
- r1 = resource.Resource()
- r1.putChild(b"there", static.Data(b"root", "text/plain"))
- site1 = server.Site(r1)
- self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
- self.port1 = reactor.listenTCP(0, self.f1)
- self.sub = distrib.ResourceSubscription("127.0.0.1",
- self.port1.getHost().port)
- r2 = resource.Resource()
- r2.putChild(b"here", self.sub)
- f2 = MySite(r2)
- self.port2 = reactor.listenTCP(0, f2)
- agent = client.Agent(reactor)
- url = "http://127.0.0.1:{}/here/there".format(
- self.port2.getHost().port)
- url = url.encode("ascii")
- d = agent.request(b"GET", url)
- d.addCallback(client.readBody)
- d.addCallback(self.assertEqual, b'root')
- return d
- def _setupDistribServer(self, child):
- """
- Set up a resource on a distrib site using L{ResourcePublisher}.
- @param child: The resource to publish using distrib.
- @return: A tuple consisting of the host and port on which to contact
- the created site.
- """
- distribRoot = resource.Resource()
- distribRoot.putChild(b"child", child)
- distribSite = server.Site(distribRoot)
- self.f1 = distribFactory = PBServerFactory(
- distrib.ResourcePublisher(distribSite))
- distribPort = reactor.listenTCP(
- 0, distribFactory, interface="127.0.0.1")
- self.addCleanup(distribPort.stopListening)
- addr = distribPort.getHost()
- self.sub = mainRoot = distrib.ResourceSubscription(
- addr.host, addr.port)
- mainSite = server.Site(mainRoot)
- mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
- self.addCleanup(mainPort.stopListening)
- mainAddr = mainPort.getHost()
- return mainPort, mainAddr
- def _requestTest(self, child, **kwargs):
- """
- Set up a resource on a distrib site using L{ResourcePublisher} and
- then retrieve it from a L{ResourceSubscription} via an HTTP client.
- @param child: The resource to publish using distrib.
- @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
- requesting the resource.
- @return: A L{Deferred} which fires with the result of the request.
- """
- mainPort, mainAddr = self._setupDistribServer(child)
- agent = client.Agent(reactor)
- url = "http://%s:%s/child" % (mainAddr.host, mainAddr.port)
- url = url.encode("ascii")
- d = agent.request(b"GET", url, **kwargs)
- d.addCallback(client.readBody)
- return d
- def _requestAgentTest(self, child, **kwargs):
- """
- Set up a resource on a distrib site using L{ResourcePublisher} and
- then retrieve it from a L{ResourceSubscription} via an HTTP client.
- @param child: The resource to publish using distrib.
- @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
- requesting the resource.
- @return: A L{Deferred} which fires with a tuple consisting of a
- L{twisted.test.proto_helpers.AccumulatingProtocol} containing the
- body of the response and an L{IResponse} with the response itself.
- """
- mainPort, mainAddr = self._setupDistribServer(child)
- url = "http://{}:{}/child".format(mainAddr.host, mainAddr.port)
- url = url.encode("ascii")
- d = client.Agent(reactor).request(b"GET", url, **kwargs)
- def cbCollectBody(response):
- protocol = proto_helpers.AccumulatingProtocol()
- response.deliverBody(protocol)
- d = protocol.closedDeferred = defer.Deferred()
- d.addCallback(lambda _: (protocol, response))
- return d
- d.addCallback(cbCollectBody)
- return d
- def test_requestHeaders(self):
- """
- The request headers are available on the request object passed to a
- distributed resource's C{render} method.
- """
- requestHeaders = {}
- class ReportRequestHeaders(resource.Resource):
- def render(self, request):
- requestHeaders.update(dict(
- request.requestHeaders.getAllRawHeaders()))
- return b""
- request = self._requestTest(
- ReportRequestHeaders(), headers=Headers({'foo': ['bar']}))
- def cbRequested(result):
- self.assertEqual(requestHeaders[b'Foo'], [b'bar'])
- request.addCallback(cbRequested)
- return request
- def test_requestResponseCode(self):
- """
- The response code can be set by the request object passed to a
- distributed resource's C{render} method.
- """
- class SetResponseCode(resource.Resource):
- def render(self, request):
- request.setResponseCode(200)
- return ""
- request = self._requestAgentTest(SetResponseCode())
- def cbRequested(result):
- self.assertEqual(result[0].data, b"")
- self.assertEqual(result[1].code, 200)
- self.assertEqual(result[1].phrase, b"OK")
- request.addCallback(cbRequested)
- return request
- def test_requestResponseCodeMessage(self):
- """
- The response code and message can be set by the request object passed to
- a distributed resource's C{render} method.
- """
- class SetResponseCode(resource.Resource):
- def render(self, request):
- request.setResponseCode(200, b"some-message")
- return ""
- request = self._requestAgentTest(SetResponseCode())
- def cbRequested(result):
- self.assertEqual(result[0].data, b"")
- self.assertEqual(result[1].code, 200)
- self.assertEqual(result[1].phrase, b"some-message")
- request.addCallback(cbRequested)
- return request
- def test_largeWrite(self):
- """
- If a string longer than the Banana size limit is passed to the
- L{distrib.Request} passed to the remote resource, it is broken into
- smaller strings to be transported over the PB connection.
- """
- class LargeWrite(resource.Resource):
- def render(self, request):
- request.write(b'x' * SIZE_LIMIT + b'y')
- request.finish()
- return server.NOT_DONE_YET
- request = self._requestTest(LargeWrite())
- request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
- return request
- def test_largeReturn(self):
- """
- Like L{test_largeWrite}, but for the case where C{render} returns a
- long string rather than explicitly passing it to L{Request.write}.
- """
- class LargeReturn(resource.Resource):
- def render(self, request):
- return b'x' * SIZE_LIMIT + b'y'
- request = self._requestTest(LargeReturn())
- request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
- return request
- def test_connectionLost(self):
- """
- If there is an error issuing the request to the remote publisher, an
- error response is returned.
- """
- # Using pb.Root as a publisher will cause request calls to fail with an
- # error every time. Just what we want to test.
- self.f1 = serverFactory = PBServerFactory(pb.Root())
- self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
- self.sub = subscription = distrib.ResourceSubscription(
- "127.0.0.1", serverPort.getHost().port)
- request = DummyRequest([b''])
- d = _render(subscription, request)
- def cbRendered(ignored):
- self.assertEqual(request.responseCode, 500)
- # This is the error we caused the request to fail with. It should
- # have been logged.
- errors = self.flushLoggedErrors(pb.NoSuchMethod)
- self.assertEqual(len(errors), 1)
- # The error page is rendered as HTML.
- expected = [
- b'',
- b'<html>',
- b' <head><title>500 - Server Connection Lost</title></head>',
- b' <body>',
- b' <h1>Server Connection Lost</h1>',
- b' <p>Connection to distributed server lost:'
- b'<pre>'
- b'[Failure instance: Traceback from remote host -- '
- b'twisted.spread.flavors.NoSuchMethod: '
- b'No such method: remote_request',
- b']</pre></p>',
- b' </body>',
- b'</html>',
- b''
- ]
- self.assertEqual([b'\n'.join(expected)], request.written)
- d.addCallback(cbRendered)
- return d
- class _PasswordDatabase:
- def __init__(self, users):
- self._users = users
- def getpwall(self):
- return iter(self._users)
- def getpwnam(self, username):
- for user in self._users:
- if user[0] == username:
- return user
- raise KeyError()
- class UserDirectoryTests(unittest.TestCase):
- """
- Tests for L{UserDirectory}, a resource for listing all user resources
- available on a system.
- """
- def setUp(self):
- self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
- self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
- self.database = _PasswordDatabase([self.alice, self.bob])
- self.directory = distrib.UserDirectory(self.database)
- def test_interface(self):
- """
- L{UserDirectory} instances provide L{resource.IResource}.
- """
- self.assertTrue(verifyObject(resource.IResource, self.directory))
- def _404Test(self, name):
- """
- Verify that requesting the C{name} child of C{self.directory} results
- in a 404 response.
- """
- request = DummyRequest([name])
- result = self.directory.getChild(name, request)
- d = _render(result, request)
- def cbRendered(ignored):
- self.assertEqual(request.responseCode, 404)
- d.addCallback(cbRendered)
- return d
- def test_getInvalidUser(self):
- """
- L{UserDirectory.getChild} returns a resource which renders a 404
- response when passed a string which does not correspond to any known
- user.
- """
- return self._404Test('carol')
- def test_getUserWithoutResource(self):
- """
- L{UserDirectory.getChild} returns a resource which renders a 404
- response when passed a string which corresponds to a known user who has
- neither a user directory nor a user distrib socket.
- """
- return self._404Test('alice')
- def test_getPublicHTMLChild(self):
- """
- L{UserDirectory.getChild} returns a L{static.File} instance when passed
- the name of a user with a home directory containing a I{public_html}
- directory.
- """
- home = filepath.FilePath(self.bob[-2])
- public_html = home.child('public_html')
- public_html.makedirs()
- request = DummyRequest(['bob'])
- result = self.directory.getChild('bob', request)
- self.assertIsInstance(result, static.File)
- self.assertEqual(result.path, public_html.path)
- def test_getDistribChild(self):
- """
- L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
- when passed the name of a user suffixed with C{".twistd"} who has a
- home directory containing a I{.twistd-web-pb} socket.
- """
- home = filepath.FilePath(self.bob[-2])
- home.makedirs()
- web = home.child('.twistd-web-pb')
- request = DummyRequest(['bob'])
- result = self.directory.getChild('bob.twistd', request)
- self.assertIsInstance(result, distrib.ResourceSubscription)
- self.assertEqual(result.host, 'unix')
- self.assertEqual(abspath(result.port), web.path)
- def test_invalidMethod(self):
- """
- L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
- non-I{GET} request.
- """
- request = DummyRequest([''])
- request.method = 'POST'
- self.assertRaises(
- server.UnsupportedMethod, self.directory.render, request)
- def test_render(self):
- """
- L{UserDirectory} renders a list of links to available user content
- in response to a I{GET} request.
- """
- public_html = filepath.FilePath(self.alice[-2]).child('public_html')
- public_html.makedirs()
- web = filepath.FilePath(self.bob[-2])
- web.makedirs()
- # This really only works if it's a unix socket, but the implementation
- # doesn't currently check for that. It probably should someday, and
- # then skip users with non-sockets.
- web.child('.twistd-web-pb').setContent(b"")
- request = DummyRequest([''])
- result = _render(self.directory, request)
- def cbRendered(ignored):
- document = parseString(b''.join(request.written))
- # Each user should have an li with a link to their page.
- [alice, bob] = document.getElementsByTagName('li')
- self.assertEqual(alice.firstChild.tagName, 'a')
- self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
- self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
- self.assertEqual(bob.firstChild.tagName, 'a')
- self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
- self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
- result.addCallback(cbRendered)
- return result
- def test_passwordDatabase(self):
- """
- If L{UserDirectory} is instantiated with no arguments, it uses the
- L{pwd} module as its password database.
- """
- directory = distrib.UserDirectory()
- self.assertIdentical(directory._pwd, pwd)
- if pwd is None:
- test_passwordDatabase.skip = "pwd module required"
|