17.12
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
General helpers for L{twisted.web} unit tests.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.internet.defer import succeed
|
||||
from twisted.web import server
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
from twisted.web._flatten import flattenString
|
||||
from twisted.web.error import FlattenerError
|
||||
|
||||
|
||||
|
||||
def _render(resource, request):
|
||||
result = resource.render(request)
|
||||
if isinstance(result, bytes):
|
||||
request.write(result)
|
||||
request.finish()
|
||||
return succeed(None)
|
||||
elif result is server.NOT_DONE_YET:
|
||||
if request.finished:
|
||||
return succeed(None)
|
||||
else:
|
||||
return request.notifyFinish()
|
||||
else:
|
||||
raise ValueError("Unexpected return value: %r" % (result,))
|
||||
|
||||
|
||||
|
||||
class FlattenTestCase(TestCase):
|
||||
"""
|
||||
A test case that assists with testing L{twisted.web._flatten}.
|
||||
"""
|
||||
def assertFlattensTo(self, root, target):
|
||||
"""
|
||||
Assert that a root element, when flattened, is equal to a string.
|
||||
"""
|
||||
d = flattenString(None, root)
|
||||
d.addCallback(lambda s: self.assertEqual(s, target))
|
||||
return d
|
||||
|
||||
|
||||
def assertFlattensImmediately(self, root, target):
|
||||
"""
|
||||
Assert that a root element, when flattened, is equal to a string, and
|
||||
performs no asynchronus Deferred anything.
|
||||
|
||||
This version is more convenient in tests which wish to make multiple
|
||||
assertions about flattening, since it can be called multiple times
|
||||
without having to add multiple callbacks.
|
||||
|
||||
@return: the result of rendering L{root}, which should be equivalent to
|
||||
L{target}.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
results = []
|
||||
it = self.assertFlattensTo(root, target)
|
||||
it.addBoth(results.append)
|
||||
# Do our best to clean it up if something goes wrong.
|
||||
self.addCleanup(it.cancel)
|
||||
if not results:
|
||||
self.fail("Rendering did not complete immediately.")
|
||||
result = results[0]
|
||||
if isinstance(result, Failure):
|
||||
result.raiseException()
|
||||
return results[0]
|
||||
|
||||
|
||||
def assertFlatteningRaises(self, root, exn):
|
||||
"""
|
||||
Assert flattening a root element raises a particular exception.
|
||||
"""
|
||||
d = self.assertFailure(self.assertFlattensTo(root, b''), FlattenerError)
|
||||
d.addCallback(lambda exc: self.assertIsInstance(exc._exception, exn))
|
||||
return d
|
||||
|
||||
|
||||
|
||||
def assertIsFilesystemTemporary(case, fileObj):
|
||||
"""
|
||||
Assert that C{fileObj} is a temporary file on the filesystem.
|
||||
|
||||
@param case: A C{TestCase} instance to use to make the assertion.
|
||||
|
||||
@raise: C{case.failureException} if C{fileObj} is not a temporary file on
|
||||
the filesystem.
|
||||
"""
|
||||
# The tempfile API used to create content returns an instance of a
|
||||
# different type depending on what platform we're running on. The point
|
||||
# here is to verify that the request body is in a file that's on the
|
||||
# filesystem. Having a fileno method that returns an int is a somewhat
|
||||
# close approximation of this. -exarkun
|
||||
case.assertIsInstance(fileObj.fileno(), int)
|
||||
|
||||
|
||||
|
||||
__all__ = ["_render", "FlattenTestCase", "assertIsFilesystemTemporary"]
|
||||
@@ -0,0 +1,527 @@
|
||||
# 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, failure
|
||||
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, DummyChannel
|
||||
from twisted.web.test._util import _render
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.logger import globalLogPublisher
|
||||
|
||||
|
||||
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 ArbitraryError(Exception):
|
||||
"""
|
||||
An exception for this test.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
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 = {}
|
||||
logObserver = proto_helpers.EventLoggingObserver()
|
||||
globalLogPublisher.addObserver(logObserver)
|
||||
req = [None]
|
||||
|
||||
|
||||
class ReportRequestHeaders(resource.Resource):
|
||||
def render(self, request):
|
||||
req[0] = request
|
||||
requestHeaders.update(dict(
|
||||
request.requestHeaders.getAllRawHeaders()))
|
||||
return b""
|
||||
|
||||
def check_logs():
|
||||
msgs = [e["log_format"] for e in logObserver]
|
||||
self.assertIn('connected to publisher', msgs)
|
||||
self.assertIn(
|
||||
"could not connect to distributed web service: {msg}",
|
||||
msgs
|
||||
)
|
||||
self.assertIn(req[0], msgs)
|
||||
globalLogPublisher.removeObserver(logObserver)
|
||||
|
||||
request = self._requestTest(
|
||||
ReportRequestHeaders(), headers=Headers({'foo': ['bar']}))
|
||||
def cbRequested(result):
|
||||
self.f1.proto.notifyOnDisconnect(check_logs)
|
||||
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
|
||||
|
||||
|
||||
def test_logFailed(self):
|
||||
"""
|
||||
When a request fails, the string form of the failure is logged.
|
||||
"""
|
||||
logObserver = proto_helpers.EventLoggingObserver.createWithCleanup(
|
||||
self,
|
||||
globalLogPublisher
|
||||
)
|
||||
|
||||
f = failure.Failure(ArbitraryError())
|
||||
request = DummyRequest([b''])
|
||||
issue = distrib.Issue(request)
|
||||
issue.failed(f)
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIn(
|
||||
"Failure instance",
|
||||
logObserver[0]["log_format"]
|
||||
)
|
||||
|
||||
|
||||
def test_requestFail(self):
|
||||
"""
|
||||
When L{twisted.web.distrib.Request}'s fail is called, the failure
|
||||
is logged.
|
||||
"""
|
||||
logObserver = proto_helpers.EventLoggingObserver.createWithCleanup(
|
||||
self,
|
||||
globalLogPublisher
|
||||
)
|
||||
err = ArbitraryError()
|
||||
f = failure.Failure(err)
|
||||
req = distrib.Request(DummyChannel())
|
||||
req.fail(f)
|
||||
self.flushLoggedErrors(ArbitraryError)
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIs(logObserver[0]["log_failure"], f)
|
||||
|
||||
|
||||
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import html
|
||||
|
||||
|
||||
|
||||
class WebHtmlTests(unittest.TestCase):
|
||||
"""
|
||||
Unit tests for L{twisted.web.html}.
|
||||
"""
|
||||
|
||||
def test_deprecation(self):
|
||||
"""
|
||||
Calls to L{twisted.web.html} members emit a deprecation warning.
|
||||
"""
|
||||
def assertDeprecationWarningOf(method):
|
||||
"""
|
||||
Check that a deprecation warning is present.
|
||||
"""
|
||||
warningsShown = self.flushWarnings([self.test_deprecation])
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
self.assertIdentical(
|
||||
warningsShown[0]['category'], DeprecationWarning)
|
||||
self.assertEqual(
|
||||
warningsShown[0]['message'],
|
||||
'twisted.web.html.%s was deprecated in Twisted 15.3.0; '
|
||||
'please use twisted.web.template instead' % (
|
||||
method,),
|
||||
)
|
||||
|
||||
html.PRE('')
|
||||
assertDeprecationWarningOf('PRE')
|
||||
|
||||
html.UL([])
|
||||
assertDeprecationWarningOf('UL')
|
||||
|
||||
html.linkList([])
|
||||
assertDeprecationWarningOf('linkList')
|
||||
|
||||
html.output(lambda: None)
|
||||
assertDeprecationWarningOf('output')
|
||||
@@ -0,0 +1,660 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.http_headers}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.compat import _PY3, unicode
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.test.requesthelper import (
|
||||
bytesLinearWhitespaceComponents,
|
||||
sanitizedBytes,
|
||||
textLinearWhitespaceComponents,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def assertSanitized(testCase, components, expected):
|
||||
"""
|
||||
Assert that the components are sanitized to the expected value as
|
||||
both a header name and value, across all of L{Header}'s setters
|
||||
and getters.
|
||||
|
||||
@param testCase: A test case.
|
||||
|
||||
@param components: A sequence of values that contain linear
|
||||
whitespace to use as header names and values; see
|
||||
C{textLinearWhitespaceComponents} and
|
||||
C{bytesLinearWhitespaceComponents}
|
||||
|
||||
@param expected: The expected sanitized form of the component for
|
||||
both headers names and their values.
|
||||
"""
|
||||
for component in components:
|
||||
headers = []
|
||||
headers.append(Headers({component: [component]}))
|
||||
|
||||
added = Headers()
|
||||
added.addRawHeader(component, component)
|
||||
headers.append(added)
|
||||
|
||||
setHeader = Headers()
|
||||
setHeader.setRawHeaders(component, [component])
|
||||
headers.append(setHeader)
|
||||
|
||||
for header in headers:
|
||||
testCase.assertEqual(list(header.getAllRawHeaders()),
|
||||
[(expected, [expected])])
|
||||
testCase.assertEqual(header.getRawHeaders(expected), [expected])
|
||||
|
||||
|
||||
|
||||
class BytesHeadersTests(TestCase):
|
||||
"""
|
||||
Tests for L{Headers}, using L{bytes} arguments for methods.
|
||||
"""
|
||||
def test_sanitizeLinearWhitespace(self):
|
||||
"""
|
||||
Linear whitespace in header names or values is replaced with a
|
||||
single space.
|
||||
"""
|
||||
assertSanitized(self, bytesLinearWhitespaceComponents, sanitizedBytes)
|
||||
|
||||
|
||||
def test_initializer(self):
|
||||
"""
|
||||
The header values passed to L{Headers.__init__} can be retrieved via
|
||||
L{Headers.getRawHeaders}.
|
||||
"""
|
||||
h = Headers({b'Foo': [b'bar']})
|
||||
self.assertEqual(h.getRawHeaders(b'foo'), [b'bar'])
|
||||
|
||||
|
||||
def test_setRawHeaders(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} sets the header values for the given
|
||||
header name to the sequence of byte string values.
|
||||
"""
|
||||
rawValue = [b"value1", b"value2"]
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", rawValue)
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
self.assertEqual(h.getRawHeaders(b"test"), rawValue)
|
||||
|
||||
|
||||
def test_rawHeadersTypeChecking(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} requires values to be of type list.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertRaises(TypeError, h.setRawHeaders, b'key', {b'Foo': b'bar'})
|
||||
|
||||
|
||||
def test_addRawHeader(self):
|
||||
"""
|
||||
L{Headers.addRawHeader} adds a new value for a given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.addRawHeader(b"test", b"lemur")
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur"])
|
||||
h.addRawHeader(b"test", b"panda")
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur", b"panda"])
|
||||
|
||||
|
||||
def test_getRawHeadersNoDefault(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns L{None} if the header is not found and
|
||||
no default is specified.
|
||||
"""
|
||||
self.assertIsNone(Headers().getRawHeaders(b"test"))
|
||||
|
||||
|
||||
def test_getRawHeadersDefaultValue(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the specified default value when no
|
||||
header is found.
|
||||
"""
|
||||
h = Headers()
|
||||
default = object()
|
||||
self.assertIdentical(h.getRawHeaders(b"test", default), default)
|
||||
|
||||
|
||||
def test_getRawHeadersWithDefaultMatchingValue(self):
|
||||
"""
|
||||
If the object passed as the value list to L{Headers.setRawHeaders}
|
||||
is later passed as a default to L{Headers.getRawHeaders}, the
|
||||
result nevertheless contains encoded values.
|
||||
"""
|
||||
h = Headers()
|
||||
default = [u"value"]
|
||||
h.setRawHeaders(b"key", default)
|
||||
self.assertIsInstance(h.getRawHeaders(b"key", default)[0], bytes)
|
||||
self.assertEqual(h.getRawHeaders(b"key", default), [b"value"])
|
||||
|
||||
|
||||
def test_getRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the values which have been set for a
|
||||
given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"Test"), [b"lemur"])
|
||||
|
||||
|
||||
def test_hasHeaderTrue(self):
|
||||
"""
|
||||
Check that L{Headers.hasHeader} returns C{True} when the given header
|
||||
is found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemur"])
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
|
||||
|
||||
def test_hasHeaderFalse(self):
|
||||
"""
|
||||
L{Headers.hasHeader} returns C{False} when the given header is not
|
||||
found.
|
||||
"""
|
||||
self.assertFalse(Headers().hasHeader(b"test"))
|
||||
|
||||
|
||||
def test_removeHeader(self):
|
||||
"""
|
||||
Check that L{Headers.removeHeader} removes the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
h.setRawHeaders(b"foo", [b"lemur"])
|
||||
self.assertTrue(h.hasHeader(b"foo"))
|
||||
h.removeHeader(b"foo")
|
||||
self.assertFalse(h.hasHeader(b"foo"))
|
||||
|
||||
h.setRawHeaders(b"bar", [b"panda"])
|
||||
self.assertTrue(h.hasHeader(b"bar"))
|
||||
h.removeHeader(b"Bar")
|
||||
self.assertFalse(h.hasHeader(b"bar"))
|
||||
|
||||
|
||||
def test_removeHeaderDoesntExist(self):
|
||||
"""
|
||||
L{Headers.removeHeader} is a no-operation when the specified header is
|
||||
not found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.removeHeader(b"test")
|
||||
self.assertEqual(list(h.getAllRawHeaders()), [])
|
||||
|
||||
|
||||
def test_canonicalNameCaps(self):
|
||||
"""
|
||||
L{Headers._canonicalNameCaps} returns the canonical capitalization for
|
||||
the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertEqual(h._canonicalNameCaps(b"test"), b"Test")
|
||||
self.assertEqual(h._canonicalNameCaps(b"test-stuff"), b"Test-Stuff")
|
||||
self.assertEqual(h._canonicalNameCaps(b"content-md5"), b"Content-MD5")
|
||||
self.assertEqual(h._canonicalNameCaps(b"dnt"), b"DNT")
|
||||
self.assertEqual(h._canonicalNameCaps(b"etag"), b"ETag")
|
||||
self.assertEqual(h._canonicalNameCaps(b"p3p"), b"P3P")
|
||||
self.assertEqual(h._canonicalNameCaps(b"te"), b"TE")
|
||||
self.assertEqual(h._canonicalNameCaps(b"www-authenticate"),
|
||||
b"WWW-Authenticate")
|
||||
self.assertEqual(h._canonicalNameCaps(b"x-xss-protection"),
|
||||
b"X-XSS-Protection")
|
||||
|
||||
|
||||
def test_getAllRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getAllRawHeaders} returns an iterable of (k, v) pairs, where
|
||||
C{k} is the canonicalized representation of the header name, and C{v}
|
||||
is a sequence of values.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemurs"])
|
||||
h.setRawHeaders(b"www-authenticate", [b"basic aksljdlk="])
|
||||
|
||||
allHeaders = set([(k, tuple(v)) for k, v in h.getAllRawHeaders()])
|
||||
|
||||
self.assertEqual(allHeaders,
|
||||
set([(b"WWW-Authenticate", (b"basic aksljdlk=",)),
|
||||
(b"Test", (b"lemurs",))]))
|
||||
|
||||
|
||||
def test_headersComparison(self):
|
||||
"""
|
||||
A L{Headers} instance compares equal to itself and to another
|
||||
L{Headers} instance with the same values.
|
||||
"""
|
||||
first = Headers()
|
||||
first.setRawHeaders(b"foo", [b"panda"])
|
||||
second = Headers()
|
||||
second.setRawHeaders(b"foo", [b"panda"])
|
||||
third = Headers()
|
||||
third.setRawHeaders(b"foo", [b"lemur", b"panda"])
|
||||
self.assertEqual(first, first)
|
||||
self.assertEqual(first, second)
|
||||
self.assertNotEqual(first, third)
|
||||
|
||||
|
||||
def test_otherComparison(self):
|
||||
"""
|
||||
An instance of L{Headers} does not compare equal to other unrelated
|
||||
objects.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertNotEqual(h, ())
|
||||
self.assertNotEqual(h, object())
|
||||
self.assertNotEqual(h, b"foo")
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains.
|
||||
"""
|
||||
foo = b"foo"
|
||||
bar = b"bar"
|
||||
baz = b"baz"
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_reprWithRawBytes(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains, not attempting to decode any raw bytes.
|
||||
"""
|
||||
# There's no such thing as undecodable latin-1, you'll just get
|
||||
# some mojibake
|
||||
foo = b"foo"
|
||||
# But this is invalid UTF-8! So, any accidental decoding/encoding will
|
||||
# throw an exception.
|
||||
bar = b"bar\xe1"
|
||||
baz = b"baz\xe1"
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_subclassRepr(self):
|
||||
"""
|
||||
The L{repr} of an instance of a subclass of L{Headers} uses the name
|
||||
of the subclass instead of the string C{"Headers"}.
|
||||
"""
|
||||
foo = b"foo"
|
||||
bar = b"bar"
|
||||
baz = b"baz"
|
||||
class FunnyHeaders(Headers):
|
||||
pass
|
||||
self.assertEqual(
|
||||
repr(FunnyHeaders({foo: [bar, baz]})),
|
||||
"FunnyHeaders({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_copy(self):
|
||||
"""
|
||||
L{Headers.copy} creates a new independent copy of an existing
|
||||
L{Headers} instance, allowing future modifications without impacts
|
||||
between the copies.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b'test', [b'foo'])
|
||||
i = h.copy()
|
||||
self.assertEqual(i.getRawHeaders(b'test'), [b'foo'])
|
||||
h.addRawHeader(b'test', b'bar')
|
||||
self.assertEqual(i.getRawHeaders(b'test'), [b'foo'])
|
||||
i.addRawHeader(b'test', b'baz')
|
||||
self.assertEqual(h.getRawHeaders(b'test'), [b'foo', b'bar'])
|
||||
|
||||
|
||||
|
||||
class UnicodeHeadersTests(TestCase):
|
||||
"""
|
||||
Tests for L{Headers}, using L{unicode} arguments for methods.
|
||||
"""
|
||||
|
||||
def test_sanitizeLinearWhitespace(self):
|
||||
"""
|
||||
Linear whitespace in header names or values is replaced with a
|
||||
single space.
|
||||
"""
|
||||
assertSanitized(self, textLinearWhitespaceComponents, sanitizedBytes)
|
||||
|
||||
|
||||
def test_initializer(self):
|
||||
"""
|
||||
The header values passed to L{Headers.__init__} can be retrieved via
|
||||
L{Headers.getRawHeaders}. If a L{bytes} argument is given, it returns
|
||||
L{bytes} values, and if a L{unicode} argument is given, it returns
|
||||
L{unicode} values. Both are the same header value, just encoded or
|
||||
decoded.
|
||||
"""
|
||||
h = Headers({u'Foo': [u'bar']})
|
||||
self.assertEqual(h.getRawHeaders(b'foo'), [b'bar'])
|
||||
self.assertEqual(h.getRawHeaders(u'foo'), [u'bar'])
|
||||
|
||||
|
||||
def test_setRawHeaders(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} sets the header values for the given
|
||||
header name to the sequence of strings, encoded.
|
||||
"""
|
||||
rawValue = [u"value1", u"value2"]
|
||||
rawEncodedValue = [b"value1", b"value2"]
|
||||
h = Headers()
|
||||
h.setRawHeaders("test", rawValue)
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
self.assertTrue(h.hasHeader("test"))
|
||||
self.assertTrue(h.hasHeader("Test"))
|
||||
self.assertEqual(h.getRawHeaders("test"), rawValue)
|
||||
self.assertEqual(h.getRawHeaders(b"test"), rawEncodedValue)
|
||||
|
||||
|
||||
def test_nameNotEncodable(self):
|
||||
"""
|
||||
Passing L{unicode} to any function that takes a header name will encode
|
||||
said header name as ISO-8859-1, and if it cannot be encoded, it will
|
||||
raise a L{UnicodeDecodeError}.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
# Only these two functions take names
|
||||
with self.assertRaises(UnicodeEncodeError):
|
||||
h.setRawHeaders(u"\u2603", [u"val"])
|
||||
|
||||
with self.assertRaises(UnicodeEncodeError):
|
||||
h.hasHeader(u"\u2603")
|
||||
|
||||
|
||||
def test_nameEncoding(self):
|
||||
"""
|
||||
Passing L{unicode} to any function that takes a header name will encode
|
||||
said header name as ISO-8859-1.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
# We set it using a Unicode string.
|
||||
h.setRawHeaders(u"\u00E1", [b"foo"])
|
||||
|
||||
# It's encoded to the ISO-8859-1 value, which we can use to access it
|
||||
self.assertTrue(h.hasHeader(b"\xe1"))
|
||||
self.assertEqual(h.getRawHeaders(b"\xe1"), [b'foo'])
|
||||
|
||||
# We can still access it using the Unicode string..
|
||||
self.assertTrue(h.hasHeader(u"\u00E1"))
|
||||
|
||||
|
||||
def test_rawHeadersValueEncoding(self):
|
||||
"""
|
||||
Passing L{unicode} to L{Headers.setRawHeaders} will encode the name as
|
||||
ISO-8859-1 and values as UTF-8.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"\u00E1", [u"\u2603", b"foo"])
|
||||
self.assertTrue(h.hasHeader(b"\xe1"))
|
||||
self.assertEqual(h.getRawHeaders(b"\xe1"), [b'\xe2\x98\x83', b'foo'])
|
||||
|
||||
|
||||
def test_rawHeadersTypeChecking(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} requires values to be of type list.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertRaises(TypeError, h.setRawHeaders, u'key', {u'Foo': u'bar'})
|
||||
|
||||
|
||||
def test_addRawHeader(self):
|
||||
"""
|
||||
L{Headers.addRawHeader} adds a new value for a given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.addRawHeader(u"test", u"lemur")
|
||||
self.assertEqual(h.getRawHeaders(u"test"), [u"lemur"])
|
||||
h.addRawHeader(u"test", u"panda")
|
||||
self.assertEqual(h.getRawHeaders(u"test"), [u"lemur", u"panda"])
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur", b"panda"])
|
||||
|
||||
|
||||
def test_getRawHeadersNoDefault(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns L{None} if the header is not found and
|
||||
no default is specified.
|
||||
"""
|
||||
self.assertIsNone(Headers().getRawHeaders(u"test"))
|
||||
|
||||
|
||||
def test_getRawHeadersDefaultValue(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the specified default value when no
|
||||
header is found.
|
||||
"""
|
||||
h = Headers()
|
||||
default = object()
|
||||
self.assertIdentical(h.getRawHeaders(u"test", default), default)
|
||||
self.assertIdentical(h.getRawHeaders(u"test", None), None)
|
||||
self.assertEqual(h.getRawHeaders(u"test", [None]), [None])
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(u"test", [u"\N{SNOWMAN}"]),
|
||||
[u"\N{SNOWMAN}"],
|
||||
)
|
||||
|
||||
|
||||
def test_getRawHeadersWithDefaultMatchingValue(self):
|
||||
"""
|
||||
If the object passed as the value list to L{Headers.setRawHeaders}
|
||||
is later passed as a default to L{Headers.getRawHeaders}, the
|
||||
result nevertheless contains decoded values.
|
||||
"""
|
||||
h = Headers()
|
||||
default = [b"value"]
|
||||
h.setRawHeaders(b"key", default)
|
||||
self.assertIsInstance(h.getRawHeaders(u"key", default)[0], unicode)
|
||||
self.assertEqual(h.getRawHeaders(u"key", default), [u"value"])
|
||||
|
||||
|
||||
def test_getRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the values which have been set for a
|
||||
given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(u"test\u00E1"), [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(u"Test\u00E1"), [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"test\xe1"), [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"Test\xe1"), [b"lemur"])
|
||||
|
||||
|
||||
def test_hasHeaderTrue(self):
|
||||
"""
|
||||
Check that L{Headers.hasHeader} returns C{True} when the given header
|
||||
is found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemur"])
|
||||
self.assertTrue(h.hasHeader(u"test\u00E1"))
|
||||
self.assertTrue(h.hasHeader(u"Test\u00E1"))
|
||||
self.assertTrue(h.hasHeader(b"test\xe1"))
|
||||
self.assertTrue(h.hasHeader(b"Test\xe1"))
|
||||
|
||||
|
||||
def test_hasHeaderFalse(self):
|
||||
"""
|
||||
L{Headers.hasHeader} returns C{False} when the given header is not
|
||||
found.
|
||||
"""
|
||||
self.assertFalse(Headers().hasHeader(u"test\u00E1"))
|
||||
|
||||
|
||||
def test_removeHeader(self):
|
||||
"""
|
||||
Check that L{Headers.removeHeader} removes the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
h.setRawHeaders(u"foo", [u"lemur"])
|
||||
self.assertTrue(h.hasHeader(u"foo"))
|
||||
h.removeHeader(u"foo")
|
||||
self.assertFalse(h.hasHeader(u"foo"))
|
||||
self.assertFalse(h.hasHeader(b"foo"))
|
||||
|
||||
h.setRawHeaders(u"bar", [u"panda"])
|
||||
self.assertTrue(h.hasHeader(u"bar"))
|
||||
h.removeHeader(u"Bar")
|
||||
self.assertFalse(h.hasHeader(u"bar"))
|
||||
self.assertFalse(h.hasHeader(b"bar"))
|
||||
|
||||
|
||||
def test_removeHeaderDoesntExist(self):
|
||||
"""
|
||||
L{Headers.removeHeader} is a no-operation when the specified header is
|
||||
not found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.removeHeader(u"test")
|
||||
self.assertEqual(list(h.getAllRawHeaders()), [])
|
||||
|
||||
|
||||
def test_getAllRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getAllRawHeaders} returns an iterable of (k, v) pairs, where
|
||||
C{k} is the canonicalized representation of the header name, and C{v}
|
||||
is a sequence of values.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemurs"])
|
||||
h.setRawHeaders(u"www-authenticate", [u"basic aksljdlk="])
|
||||
h.setRawHeaders(u"content-md5", [u"kjdfdfgdfgnsd"])
|
||||
|
||||
allHeaders = set([(k, tuple(v)) for k, v in h.getAllRawHeaders()])
|
||||
|
||||
self.assertEqual(allHeaders,
|
||||
set([(b"WWW-Authenticate", (b"basic aksljdlk=",)),
|
||||
(b"Content-MD5", (b"kjdfdfgdfgnsd",)),
|
||||
(b"Test\xe1", (b"lemurs",))]))
|
||||
|
||||
|
||||
def test_headersComparison(self):
|
||||
"""
|
||||
A L{Headers} instance compares equal to itself and to another
|
||||
L{Headers} instance with the same values.
|
||||
"""
|
||||
first = Headers()
|
||||
first.setRawHeaders(u"foo\u00E1", [u"panda"])
|
||||
second = Headers()
|
||||
second.setRawHeaders(u"foo\u00E1", [u"panda"])
|
||||
third = Headers()
|
||||
third.setRawHeaders(u"foo\u00E1", [u"lemur", u"panda"])
|
||||
|
||||
self.assertEqual(first, first)
|
||||
self.assertEqual(first, second)
|
||||
self.assertNotEqual(first, third)
|
||||
|
||||
# Headers instantiated with bytes equivs are also the same
|
||||
firstBytes = Headers()
|
||||
firstBytes.setRawHeaders(b"foo\xe1", [b"panda"])
|
||||
secondBytes = Headers()
|
||||
secondBytes.setRawHeaders(b"foo\xe1", [b"panda"])
|
||||
thirdBytes = Headers()
|
||||
thirdBytes.setRawHeaders(b"foo\xe1", [b"lemur", u"panda"])
|
||||
|
||||
self.assertEqual(first, firstBytes)
|
||||
self.assertEqual(second, secondBytes)
|
||||
self.assertEqual(third, thirdBytes)
|
||||
|
||||
|
||||
def test_otherComparison(self):
|
||||
"""
|
||||
An instance of L{Headers} does not compare equal to other unrelated
|
||||
objects.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertNotEqual(h, ())
|
||||
self.assertNotEqual(h, object())
|
||||
self.assertNotEqual(h, u"foo")
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains. This shows only reprs of bytes values, as
|
||||
undecodable headers may cause an exception.
|
||||
"""
|
||||
foo = u"foo\u00E1"
|
||||
bar = u"bar\u2603"
|
||||
baz = u"baz"
|
||||
fooEncoded = "'foo\\xe1'"
|
||||
barEncoded = "'bar\\xe2\\x98\\x83'"
|
||||
if _PY3:
|
||||
fooEncoded = "b" + fooEncoded
|
||||
barEncoded = "b" + barEncoded
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%s: [%s, %r]})" % (fooEncoded,
|
||||
barEncoded,
|
||||
baz.encode('utf8')))
|
||||
|
||||
|
||||
def test_subclassRepr(self):
|
||||
"""
|
||||
The L{repr} of an instance of a subclass of L{Headers} uses the name
|
||||
of the subclass instead of the string C{"Headers"}.
|
||||
"""
|
||||
foo = u"foo\u00E1"
|
||||
bar = u"bar\u2603"
|
||||
baz = u"baz"
|
||||
fooEncoded = "'foo\\xe1'"
|
||||
barEncoded = "'bar\\xe2\\x98\\x83'"
|
||||
if _PY3:
|
||||
fooEncoded = "b" + fooEncoded
|
||||
barEncoded = "b" + barEncoded
|
||||
class FunnyHeaders(Headers):
|
||||
pass
|
||||
self.assertEqual(
|
||||
repr(FunnyHeaders({foo: [bar, baz]})),
|
||||
"FunnyHeaders({%s: [%s, %r]})" % (fooEncoded,
|
||||
barEncoded,
|
||||
baz.encode('utf8')))
|
||||
|
||||
|
||||
def test_copy(self):
|
||||
"""
|
||||
L{Headers.copy} creates a new independent copy of an existing
|
||||
L{Headers} instance, allowing future modifications without impacts
|
||||
between the copies.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u'test\u00E1', [u'foo\u2603'])
|
||||
i = h.copy()
|
||||
|
||||
# The copy contains the same value as the original
|
||||
self.assertEqual(i.getRawHeaders(u'test\u00E1'), [u'foo\u2603'])
|
||||
self.assertEqual(i.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83'])
|
||||
|
||||
# Add a header to the original
|
||||
h.addRawHeader(u'test\u00E1', u'bar')
|
||||
|
||||
# Verify that the copy has not changed
|
||||
self.assertEqual(i.getRawHeaders(u'test\u00E1'), [u'foo\u2603'])
|
||||
self.assertEqual(i.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83'])
|
||||
|
||||
# Add a header to the copy
|
||||
i.addRawHeader(u'test\u00E1', b'baz')
|
||||
|
||||
# Verify that the orignal does not have it
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(u'test\u00E1'), [u'foo\u2603', u'bar'])
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83', b'bar'])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,289 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.resource}.
|
||||
"""
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.compat import _PY3
|
||||
|
||||
from twisted.web.error import UnsupportedMethod
|
||||
from twisted.web.resource import (
|
||||
NOT_FOUND, FORBIDDEN, Resource, ErrorPage, NoResource, ForbiddenResource,
|
||||
getChildForRequest)
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.test.requesthelper import DummyRequest
|
||||
|
||||
|
||||
class ErrorPageTests(TestCase):
|
||||
"""
|
||||
Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}.
|
||||
"""
|
||||
|
||||
errorPage = ErrorPage
|
||||
noResource = NoResource
|
||||
forbiddenResource = ForbiddenResource
|
||||
|
||||
def test_getChild(self):
|
||||
"""
|
||||
The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
|
||||
called on.
|
||||
"""
|
||||
page = self.errorPage(321, "foo", "bar")
|
||||
self.assertIdentical(page.getChild(b"name", object()), page)
|
||||
|
||||
|
||||
def _pageRenderingTest(self, page, code, brief, detail):
|
||||
request = DummyRequest([b''])
|
||||
template = (
|
||||
u"\n"
|
||||
u"<html>\n"
|
||||
u" <head><title>%s - %s</title></head>\n"
|
||||
u" <body>\n"
|
||||
u" <h1>%s</h1>\n"
|
||||
u" <p>%s</p>\n"
|
||||
u" </body>\n"
|
||||
u"</html>\n")
|
||||
expected = template % (code, brief, brief, detail)
|
||||
self.assertEqual(
|
||||
page.render(request), expected.encode('utf-8'))
|
||||
self.assertEqual(request.responseCode, code)
|
||||
self.assertEqual(
|
||||
request.responseHeaders,
|
||||
Headers({b'content-type': [b'text/html; charset=utf-8']}))
|
||||
|
||||
|
||||
def test_errorPageRendering(self):
|
||||
"""
|
||||
L{ErrorPage.render} returns a C{bytes} describing the error defined by
|
||||
the response code and message passed to L{ErrorPage.__init__}. It also
|
||||
uses that response code to set the response code on the L{Request}
|
||||
passed in.
|
||||
"""
|
||||
code = 321
|
||||
brief = "brief description text"
|
||||
detail = "much longer text might go here"
|
||||
page = self.errorPage(code, brief, detail)
|
||||
self._pageRenderingTest(page, code, brief, detail)
|
||||
|
||||
|
||||
def test_noResourceRendering(self):
|
||||
"""
|
||||
L{NoResource} sets the HTTP I{NOT FOUND} code.
|
||||
"""
|
||||
detail = "long message"
|
||||
page = self.noResource(detail)
|
||||
self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)
|
||||
|
||||
|
||||
def test_forbiddenResourceRendering(self):
|
||||
"""
|
||||
L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
|
||||
"""
|
||||
detail = "longer message"
|
||||
page = self.forbiddenResource(detail)
|
||||
self._pageRenderingTest(page, FORBIDDEN, "Forbidden Resource", detail)
|
||||
|
||||
|
||||
|
||||
class DynamicChild(Resource):
|
||||
"""
|
||||
A L{Resource} to be created on the fly by L{DynamicChildren}.
|
||||
"""
|
||||
def __init__(self, path, request):
|
||||
Resource.__init__(self)
|
||||
self.path = path
|
||||
self.request = request
|
||||
|
||||
|
||||
|
||||
class DynamicChildren(Resource):
|
||||
"""
|
||||
A L{Resource} with dynamic children.
|
||||
"""
|
||||
def getChild(self, path, request):
|
||||
return DynamicChild(path, request)
|
||||
|
||||
|
||||
|
||||
class BytesReturnedRenderable(Resource):
|
||||
"""
|
||||
A L{Resource} with minimal capabilities to render a response.
|
||||
"""
|
||||
def __init__(self, response):
|
||||
"""
|
||||
@param response: A C{bytes} object giving the value to return from
|
||||
C{render_GET}.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._response = response
|
||||
|
||||
|
||||
def render_GET(self, request):
|
||||
"""
|
||||
Render a response to a I{GET} request by returning a short byte string
|
||||
to be written by the server.
|
||||
"""
|
||||
return self._response
|
||||
|
||||
|
||||
|
||||
class ImplicitAllowedMethods(Resource):
|
||||
"""
|
||||
A L{Resource} which implicitly defines its allowed methods by defining
|
||||
renderers to handle them.
|
||||
"""
|
||||
def render_GET(self, request):
|
||||
pass
|
||||
|
||||
|
||||
def render_PUT(self, request):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{Resource}.
|
||||
"""
|
||||
def test_staticChildren(self):
|
||||
"""
|
||||
L{Resource.putChild} adds a I{static} child to the resource. That child
|
||||
is returned from any call to L{Resource.getChildWithDefault} for the
|
||||
child's path.
|
||||
"""
|
||||
resource = Resource()
|
||||
child = Resource()
|
||||
sibling = Resource()
|
||||
resource.putChild(b"foo", child)
|
||||
resource.putChild(b"bar", sibling)
|
||||
self.assertIdentical(
|
||||
child, resource.getChildWithDefault(b"foo", DummyRequest([])))
|
||||
|
||||
|
||||
def test_dynamicChildren(self):
|
||||
"""
|
||||
L{Resource.getChildWithDefault} delegates to L{Resource.getChild} when
|
||||
the requested path is not associated with any static child.
|
||||
"""
|
||||
path = b"foo"
|
||||
request = DummyRequest([])
|
||||
resource = DynamicChildren()
|
||||
child = resource.getChildWithDefault(path, request)
|
||||
self.assertIsInstance(child, DynamicChild)
|
||||
self.assertEqual(child.path, path)
|
||||
self.assertIdentical(child.request, request)
|
||||
|
||||
|
||||
def test_staticChildPathType(self):
|
||||
"""
|
||||
Test that passing the wrong type to putChild results in a warning,
|
||||
and a failure in Python 3
|
||||
"""
|
||||
resource = Resource()
|
||||
child = Resource()
|
||||
sibling = Resource()
|
||||
resource.putChild(u"foo", child)
|
||||
warnings = self.flushWarnings([self.test_staticChildPathType])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("Path segment must be bytes",
|
||||
warnings[0]['message'])
|
||||
if _PY3:
|
||||
# We expect an error here because u"foo" != b"foo" on Py3k
|
||||
self.assertIsInstance(
|
||||
resource.getChildWithDefault(b"foo", DummyRequest([])),
|
||||
ErrorPage)
|
||||
|
||||
resource.putChild(None, sibling)
|
||||
warnings = self.flushWarnings([self.test_staticChildPathType])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("Path segment must be bytes",
|
||||
warnings[0]['message'])
|
||||
|
||||
|
||||
def test_defaultHEAD(self):
|
||||
"""
|
||||
When not otherwise overridden, L{Resource.render} treats a I{HEAD}
|
||||
request as if it were a I{GET} request.
|
||||
"""
|
||||
expected = b"insert response here"
|
||||
request = DummyRequest([])
|
||||
request.method = b'HEAD'
|
||||
resource = BytesReturnedRenderable(expected)
|
||||
self.assertEqual(expected, resource.render(request))
|
||||
|
||||
|
||||
def test_explicitAllowedMethods(self):
|
||||
"""
|
||||
The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
|
||||
request method has a C{allowedMethods} attribute set to the value of the
|
||||
C{allowedMethods} attribute of the L{Resource}, if it has one.
|
||||
"""
|
||||
expected = [b'GET', b'HEAD', b'PUT']
|
||||
resource = Resource()
|
||||
resource.allowedMethods = expected
|
||||
request = DummyRequest([])
|
||||
request.method = b'FICTIONAL'
|
||||
exc = self.assertRaises(UnsupportedMethod, resource.render, request)
|
||||
self.assertEqual(set(expected), set(exc.allowedMethods))
|
||||
|
||||
|
||||
def test_implicitAllowedMethods(self):
|
||||
"""
|
||||
The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
|
||||
request method has a C{allowedMethods} attribute set to a list of the
|
||||
methods supported by the L{Resource}, as determined by the
|
||||
I{render_}-prefixed methods which it defines, if C{allowedMethods} is
|
||||
not explicitly defined by the L{Resource}.
|
||||
"""
|
||||
expected = set([b'GET', b'HEAD', b'PUT'])
|
||||
resource = ImplicitAllowedMethods()
|
||||
request = DummyRequest([])
|
||||
request.method = b'FICTIONAL'
|
||||
exc = self.assertRaises(UnsupportedMethod, resource.render, request)
|
||||
self.assertEqual(expected, set(exc.allowedMethods))
|
||||
|
||||
|
||||
|
||||
|
||||
class GetChildForRequestTests(TestCase):
|
||||
"""
|
||||
Tests for L{getChildForRequest}.
|
||||
"""
|
||||
def test_exhaustedPostPath(self):
|
||||
"""
|
||||
L{getChildForRequest} returns whatever resource has been reached by the
|
||||
time the request's C{postpath} is empty.
|
||||
"""
|
||||
request = DummyRequest([])
|
||||
resource = Resource()
|
||||
result = getChildForRequest(resource, request)
|
||||
self.assertIdentical(resource, result)
|
||||
|
||||
|
||||
def test_leafResource(self):
|
||||
"""
|
||||
L{getChildForRequest} returns the first resource it encounters with a
|
||||
C{isLeaf} attribute set to C{True}.
|
||||
"""
|
||||
request = DummyRequest([b"foo", b"bar"])
|
||||
resource = Resource()
|
||||
resource.isLeaf = True
|
||||
result = getChildForRequest(resource, request)
|
||||
self.assertIdentical(resource, result)
|
||||
|
||||
|
||||
def test_postPathToPrePath(self):
|
||||
"""
|
||||
As path segments from the request are traversed, they are taken from
|
||||
C{postpath} and put into C{prepath}.
|
||||
"""
|
||||
request = DummyRequest([b"foo", b"bar"])
|
||||
root = Resource()
|
||||
child = Resource()
|
||||
child.isLeaf = True
|
||||
root.putChild(b"foo", child)
|
||||
self.assertIdentical(child, getChildForRequest(root, request))
|
||||
self.assertEqual(request.prepath, [b"foo"])
|
||||
self.assertEqual(request.postpath, [b"bar"])
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.vhost}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from twisted.internet.defer import gatherResults
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.web.http import NOT_FOUND
|
||||
from twisted.web.resource import NoResource
|
||||
from twisted.web.static import Data
|
||||
from twisted.web.server import Site
|
||||
from twisted.web.vhost import (_HostResource,
|
||||
NameVirtualHost,
|
||||
VHostMonsterResource)
|
||||
from twisted.web.test.test_web import DummyRequest
|
||||
from twisted.web.test._util import _render
|
||||
|
||||
|
||||
class HostResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{_HostResource}.
|
||||
"""
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{_HostResource.getChild} returns the proper I{Resource} for the vhost
|
||||
embedded in the URL. Verify that returning the proper I{Resource}
|
||||
required changing the I{Host} in the header.
|
||||
"""
|
||||
bazroot = Data(b'root data', "")
|
||||
bazuri = Data(b'uri data', "")
|
||||
baztest = Data(b'test data', "")
|
||||
bazuri.putChild(b'test', baztest)
|
||||
bazroot.putChild(b'uri', bazuri)
|
||||
hr = _HostResource()
|
||||
|
||||
root = NameVirtualHost()
|
||||
root.default = Data(b'default data', "")
|
||||
root.addHost(b'baz.com', bazroot)
|
||||
|
||||
request = DummyRequest([b'uri', b'test'])
|
||||
request.prepath = [b'bar', b'http', b'baz.com']
|
||||
request.site = Site(root)
|
||||
request.isSecure = lambda: False
|
||||
request.host = b''
|
||||
|
||||
step = hr.getChild(b'baz.com', request) # Consumes rest of path
|
||||
self.assertIsInstance(step, Data)
|
||||
|
||||
request = DummyRequest([b'uri', b'test'])
|
||||
step = root.getChild(b'uri', request)
|
||||
self.assertIsInstance(step, NoResource)
|
||||
|
||||
|
||||
|
||||
class NameVirtualHostTests(TestCase):
|
||||
"""
|
||||
Tests for L{NameVirtualHost}.
|
||||
"""
|
||||
def test_renderWithoutHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the
|
||||
instance's C{default} if it is not L{None} and there is no I{Host}
|
||||
header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.default = Data(b"correct result", "")
|
||||
request = DummyRequest([''])
|
||||
self.assertEqual(
|
||||
virtualHostResource.render(request), b"correct result")
|
||||
|
||||
|
||||
def test_renderWithoutHostNoDefault(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns a response with a status of I{NOT
|
||||
FOUND} if the instance's C{default} is L{None} and there is no I{Host}
|
||||
header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
request = DummyRequest([''])
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, NOT_FOUND)
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_renderWithHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the resource
|
||||
which is the value in the instance's C{host} dictionary corresponding
|
||||
to the key indicated by the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.addHost(b'example.org', Data(b"winner", ""))
|
||||
|
||||
request = DummyRequest([b''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.org')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored, request):
|
||||
self.assertEqual(b''.join(request.written), b"winner")
|
||||
d.addCallback(cbRendered, request)
|
||||
|
||||
# The port portion of the Host header should not be considered.
|
||||
requestWithPort = DummyRequest([b''])
|
||||
requestWithPort.requestHeaders.addRawHeader(b'host', b'example.org:8000')
|
||||
dWithPort = _render(virtualHostResource, requestWithPort)
|
||||
def cbRendered(ignored, requestWithPort):
|
||||
self.assertEqual(b''.join(requestWithPort.written), b"winner")
|
||||
dWithPort.addCallback(cbRendered, requestWithPort)
|
||||
|
||||
return gatherResults([d, dWithPort])
|
||||
|
||||
|
||||
def test_renderWithUnknownHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the
|
||||
instance's C{default} if it is not L{None} and there is no host
|
||||
matching the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.default = Data(b"correct data", "")
|
||||
request = DummyRequest([b''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.com')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(b''.join(request.written), b"correct data")
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_renderWithUnknownHostNoDefault(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns a response with a status of I{NOT
|
||||
FOUND} if the instance's C{default} is L{None} and there is no host
|
||||
matching the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
request = DummyRequest([''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.com')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, NOT_FOUND)
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{NameVirtualHost.getChild} returns correct I{Resource} based off
|
||||
the header and modifies I{Request} to ensure proper prepath and
|
||||
postpath are set.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
leafResource = Data(b"leaf data", "")
|
||||
leafResource.isLeaf = True
|
||||
normResource = Data(b"norm data", "")
|
||||
virtualHostResource.addHost(b'leaf.example.org', leafResource)
|
||||
virtualHostResource.addHost(b'norm.example.org', normResource)
|
||||
|
||||
request = DummyRequest([])
|
||||
request.requestHeaders.addRawHeader(b'host', b'norm.example.org')
|
||||
request.prepath = [b'']
|
||||
|
||||
self.assertIsInstance(virtualHostResource.getChild(b'', request),
|
||||
NoResource)
|
||||
self.assertEqual(request.prepath, [b''])
|
||||
self.assertEqual(request.postpath, [])
|
||||
|
||||
request = DummyRequest([])
|
||||
request.requestHeaders.addRawHeader(b'host', b'leaf.example.org')
|
||||
request.prepath = [b'']
|
||||
|
||||
self.assertIsInstance(virtualHostResource.getChild(b'', request),
|
||||
Data)
|
||||
self.assertEqual(request.prepath, [])
|
||||
self.assertEqual(request.postpath, [b''])
|
||||
|
||||
|
||||
|
||||
class VHostMonsterResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{VHostMonsterResource}.
|
||||
"""
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{VHostMonsterResource.getChild} returns I{_HostResource} and modifies
|
||||
I{Request} with correct L{Request.isSecure}.
|
||||
"""
|
||||
vhm = VHostMonsterResource()
|
||||
request = DummyRequest([])
|
||||
self.assertIsInstance(vhm.getChild(b'http', request), _HostResource)
|
||||
self.assertFalse(request.isSecure())
|
||||
|
||||
request = DummyRequest([])
|
||||
self.assertIsInstance(vhm.getChild(b'https', request), _HostResource)
|
||||
self.assertTrue(request.isSecure())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The L{_response} module contains constants for all standard HTTP codes, along
|
||||
with a mapping to the corresponding phrases.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import string
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import _responses
|
||||
|
||||
class ResponseTests(unittest.TestCase):
|
||||
|
||||
def test_constants(self):
|
||||
"""
|
||||
All constants besides C{RESPONSES} defined in L{_response} are
|
||||
integers and are keys in C{RESPONSES}.
|
||||
"""
|
||||
for sym in dir(_responses):
|
||||
if sym == 'RESPONSES':
|
||||
continue
|
||||
if all((c == '_' or c in string.ascii_uppercase) for c in sym):
|
||||
val = getattr(_responses, sym)
|
||||
self.assertIsInstance(val, int)
|
||||
self.assertIn(val, _responses.RESPONSES)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,915 @@
|
||||
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for XML-RPC support in L{twisted.web.xmlrpc}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.python.compat import nativeString, networkString, NativeStringIO
|
||||
from io import BytesIO
|
||||
|
||||
import datetime
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import xmlrpc
|
||||
from twisted.web.xmlrpc import XMLRPC, payloadTemplate, addIntrospection
|
||||
from twisted.web.xmlrpc import _QueryFactory, withRequest, xmlrpclib
|
||||
from twisted.web import server, client, http, static
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.error import ConnectionDone
|
||||
from twisted.python import failure
|
||||
from twisted.python.reflect import namedModule
|
||||
from twisted.test.proto_helpers import MemoryReactor, EventLoggingObserver
|
||||
from twisted.web.test.test_web import DummyRequest
|
||||
from twisted.logger import (globalLogPublisher, FilteringLogObserver,
|
||||
LogLevelFilterPredicate, LogLevel)
|
||||
try:
|
||||
namedModule('twisted.internet.ssl')
|
||||
except ImportError:
|
||||
sslSkip = "OpenSSL not present"
|
||||
else:
|
||||
sslSkip = None
|
||||
|
||||
|
||||
class AsyncXMLRPCTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{XMLRPC}'s support of Deferreds.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = DummyRequest([''])
|
||||
self.request.method = 'POST'
|
||||
self.request.content = NativeStringIO(
|
||||
payloadTemplate % ('async', xmlrpclib.dumps(())))
|
||||
|
||||
result = self.result = defer.Deferred()
|
||||
class AsyncResource(XMLRPC):
|
||||
def xmlrpc_async(self):
|
||||
return result
|
||||
|
||||
self.resource = AsyncResource()
|
||||
|
||||
|
||||
def test_deferredResponse(self):
|
||||
"""
|
||||
If an L{XMLRPC} C{xmlrpc_*} method returns a L{defer.Deferred}, the
|
||||
response to the request is the result of that L{defer.Deferred}.
|
||||
"""
|
||||
self.resource.render(self.request)
|
||||
self.assertEqual(self.request.written, [])
|
||||
|
||||
self.result.callback("result")
|
||||
|
||||
resp = xmlrpclib.loads(b"".join(self.request.written))
|
||||
self.assertEqual(resp, (('result',), None))
|
||||
self.assertEqual(self.request.finished, 1)
|
||||
|
||||
|
||||
def test_interruptedDeferredResponse(self):
|
||||
"""
|
||||
While waiting for the L{Deferred} returned by an L{XMLRPC} C{xmlrpc_*}
|
||||
method to fire, the connection the request was issued over may close.
|
||||
If this happens, neither C{write} nor C{finish} is called on the
|
||||
request.
|
||||
"""
|
||||
self.resource.render(self.request)
|
||||
self.request.processingFailed(
|
||||
failure.Failure(ConnectionDone("Simulated")))
|
||||
self.result.callback("result")
|
||||
self.assertEqual(self.request.written, [])
|
||||
self.assertEqual(self.request.finished, 0)
|
||||
|
||||
|
||||
|
||||
class TestRuntimeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TestValueError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Test(XMLRPC):
|
||||
|
||||
# If you add xmlrpc_ methods to this class, go change test_listMethods
|
||||
# below.
|
||||
|
||||
FAILURE = 666
|
||||
NOT_FOUND = 23
|
||||
SESSION_EXPIRED = 42
|
||||
|
||||
def xmlrpc_echo(self, arg):
|
||||
return arg
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_add(self, a, b):
|
||||
"""
|
||||
This function add two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
xmlrpc_add.signature = [['int', 'int', 'int'],
|
||||
['double', 'double', 'double']]
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_pair(self, string, num):
|
||||
"""
|
||||
This function puts the two arguments in an array.
|
||||
"""
|
||||
return [string, num]
|
||||
|
||||
xmlrpc_pair.signature = [['array', 'string', 'int']]
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_defer(self, x):
|
||||
"""Help for defer."""
|
||||
return defer.succeed(x)
|
||||
|
||||
def xmlrpc_deferFail(self):
|
||||
return defer.fail(TestValueError())
|
||||
|
||||
# don't add a doc string, it's part of the test
|
||||
def xmlrpc_fail(self):
|
||||
raise TestRuntimeError
|
||||
|
||||
def xmlrpc_fault(self):
|
||||
return xmlrpc.Fault(12, "hello")
|
||||
|
||||
def xmlrpc_deferFault(self):
|
||||
return defer.fail(xmlrpc.Fault(17, "hi"))
|
||||
|
||||
def xmlrpc_snowman(self, payload):
|
||||
"""
|
||||
Used to test that we can pass Unicode.
|
||||
"""
|
||||
snowman = u"\u2603"
|
||||
if snowman != payload:
|
||||
return xmlrpc.Fault(13, "Payload not unicode snowman")
|
||||
return snowman
|
||||
|
||||
def xmlrpc_complex(self):
|
||||
return {"a": ["b", "c", 12, []], "D": "foo"}
|
||||
|
||||
def xmlrpc_dict(self, map, key):
|
||||
return map[key]
|
||||
xmlrpc_dict.help = 'Help for dict.'
|
||||
|
||||
@withRequest
|
||||
def xmlrpc_withRequest(self, request, other):
|
||||
"""
|
||||
A method decorated with L{withRequest} which can be called by
|
||||
a test to verify that the request object really is passed as
|
||||
an argument.
|
||||
"""
|
||||
return (
|
||||
# as a proof that request is a request
|
||||
request.method +
|
||||
# plus proof other arguments are still passed along
|
||||
' ' + other)
|
||||
|
||||
|
||||
def lookupProcedure(self, procedurePath):
|
||||
try:
|
||||
return XMLRPC.lookupProcedure(self, procedurePath)
|
||||
except xmlrpc.NoSuchFunction:
|
||||
if procedurePath.startswith("SESSION"):
|
||||
raise xmlrpc.Fault(self.SESSION_EXPIRED,
|
||||
"Session non-existent/expired.")
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
||||
class TestLookupProcedure(XMLRPC):
|
||||
"""
|
||||
This is a resource which customizes procedure lookup to be used by the tests
|
||||
of support for this customization.
|
||||
"""
|
||||
def echo(self, x):
|
||||
return x
|
||||
|
||||
|
||||
def lookupProcedure(self, procedureName):
|
||||
"""
|
||||
Lookup a procedure from a fixed set of choices, either I{echo} or
|
||||
I{system.listeMethods}.
|
||||
"""
|
||||
if procedureName == 'echo':
|
||||
return self.echo
|
||||
raise xmlrpc.NoSuchFunction(
|
||||
self.NOT_FOUND, 'procedure %s not found' % (procedureName,))
|
||||
|
||||
|
||||
|
||||
class TestListProcedures(XMLRPC):
|
||||
"""
|
||||
This is a resource which customizes procedure enumeration to be used by the
|
||||
tests of support for this customization.
|
||||
"""
|
||||
def listProcedures(self):
|
||||
"""
|
||||
Return a list of a single method this resource will claim to support.
|
||||
"""
|
||||
return ['foo']
|
||||
|
||||
|
||||
|
||||
class TestAuthHeader(Test):
|
||||
"""
|
||||
This is used to get the header info so that we can test
|
||||
authentication.
|
||||
"""
|
||||
def __init__(self):
|
||||
Test.__init__(self)
|
||||
self.request = None
|
||||
|
||||
def render(self, request):
|
||||
self.request = request
|
||||
return Test.render(self, request)
|
||||
|
||||
def xmlrpc_authinfo(self):
|
||||
return self.request.getUser(), self.request.getPassword()
|
||||
|
||||
|
||||
|
||||
class TestQueryProtocol(xmlrpc.QueryProtocol):
|
||||
"""
|
||||
QueryProtocol for tests that saves headers received and sent,
|
||||
inside the factory.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
self.factory.transport = self.transport
|
||||
xmlrpc.QueryProtocol.connectionMade(self)
|
||||
|
||||
def handleHeader(self, key, val):
|
||||
self.factory.headers[key.lower()] = val
|
||||
|
||||
def sendHeader(self, key, val):
|
||||
"""
|
||||
Keep sent headers so we can inspect them later.
|
||||
"""
|
||||
self.factory.sent_headers[key.lower()] = val
|
||||
xmlrpc.QueryProtocol.sendHeader(self, key, val)
|
||||
|
||||
|
||||
|
||||
class TestQueryFactory(xmlrpc._QueryFactory):
|
||||
"""
|
||||
QueryFactory using L{TestQueryProtocol} for saving headers.
|
||||
"""
|
||||
protocol = TestQueryProtocol
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.headers = {}
|
||||
self.sent_headers = {}
|
||||
xmlrpc._QueryFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
class TestQueryFactoryCancel(xmlrpc._QueryFactory):
|
||||
"""
|
||||
QueryFactory that saves a reference to the
|
||||
L{twisted.internet.interfaces.IConnector} to test connection lost.
|
||||
"""
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
self.connector = connector
|
||||
|
||||
|
||||
class XMLRPCTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.p = reactor.listenTCP(0, server.Site(Test()),
|
||||
interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
def tearDown(self):
|
||||
self.factories = []
|
||||
return self.p.stopListening()
|
||||
|
||||
def queryFactory(self, *args, **kwargs):
|
||||
"""
|
||||
Specific queryFactory for proxy that uses our custom
|
||||
L{TestQueryFactory}, and save factories.
|
||||
"""
|
||||
factory = TestQueryFactory(*args, **kwargs)
|
||||
self.factories.append(factory)
|
||||
return factory
|
||||
|
||||
def proxy(self, factory=None):
|
||||
"""
|
||||
Return a new xmlrpc.Proxy for the test site created in
|
||||
setUp(), using the given factory as the queryFactory, or
|
||||
self.queryFactory if no factory is provided.
|
||||
"""
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % self.port))
|
||||
if factory is None:
|
||||
p.queryFactory = self.queryFactory
|
||||
else:
|
||||
p.queryFactory = factory
|
||||
return p
|
||||
|
||||
def test_results(self):
|
||||
inputOutput = [
|
||||
("add", (2, 3), 5),
|
||||
("defer", ("a",), "a"),
|
||||
("dict", ({"a": 1}, "a"), 1),
|
||||
("pair", ("a", 1), ["a", 1]),
|
||||
("snowman", (u"\u2603"), u"\u2603"),
|
||||
("complex", (), {"a": ["b", "c", 12, []], "D": "foo"})]
|
||||
|
||||
dl = []
|
||||
for meth, args, outp in inputOutput:
|
||||
d = self.proxy().callRemote(meth, *args)
|
||||
d.addCallback(self.assertEqual, outp)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
|
||||
def test_headers(self):
|
||||
"""
|
||||
Verify that headers sent from the client side and the ones we
|
||||
get back from the server side are correct.
|
||||
|
||||
"""
|
||||
d = self.proxy().callRemote("snowman", u"\u2603")
|
||||
|
||||
def check_server_headers(ing):
|
||||
self.assertEqual(
|
||||
self.factories[0].headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEqual(
|
||||
self.factories[0].headers[b'content-length'], b'129')
|
||||
|
||||
def check_client_headers(ign):
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'user-agent'],
|
||||
b'Twisted/XMLRPClib')
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'content-length'], b'155')
|
||||
|
||||
d.addCallback(check_server_headers)
|
||||
d.addCallback(check_client_headers)
|
||||
return d
|
||||
|
||||
|
||||
def test_errors(self):
|
||||
"""
|
||||
Verify that for each way a method exposed via XML-RPC can fail, the
|
||||
correct 'Content-type' header is set in the response and that the
|
||||
client-side Deferred is errbacked with an appropriate C{Fault}
|
||||
instance.
|
||||
"""
|
||||
logObserver = EventLoggingObserver()
|
||||
filtered = FilteringLogObserver(
|
||||
logObserver,
|
||||
[LogLevelFilterPredicate(defaultLogLevel=LogLevel.critical)]
|
||||
)
|
||||
globalLogPublisher.addObserver(filtered)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(filtered))
|
||||
dl = []
|
||||
for code, methodName in [(666, "fail"), (666, "deferFail"),
|
||||
(12, "fault"), (23, "noSuchMethod"),
|
||||
(17, "deferFault"), (42, "SESSION_TEST")]:
|
||||
d = self.proxy().callRemote(methodName)
|
||||
d = self.assertFailure(d, xmlrpc.Fault)
|
||||
d.addCallback(lambda exc, code=code:
|
||||
self.assertEqual(exc.faultCode, code))
|
||||
dl.append(d)
|
||||
d = defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
def cb(ign):
|
||||
for factory in self.factories:
|
||||
self.assertEqual(factory.headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEquals(2, len(logObserver))
|
||||
f1 = logObserver[0]["log_failure"].value
|
||||
f2 = logObserver[1]["log_failure"].value
|
||||
|
||||
if isinstance(f1, TestValueError):
|
||||
self.assertIsInstance(f2, TestRuntimeError)
|
||||
else:
|
||||
self.assertIsInstance(f1, TestRuntimeError)
|
||||
self.assertIsInstance(f2, TestValueError)
|
||||
|
||||
self.flushLoggedErrors(TestRuntimeError, TestValueError)
|
||||
d.addCallback(cb)
|
||||
return d
|
||||
|
||||
|
||||
def test_cancel(self):
|
||||
"""
|
||||
A deferred from the Proxy can be cancelled, disconnecting
|
||||
the L{twisted.internet.interfaces.IConnector}.
|
||||
"""
|
||||
def factory(*args, **kw):
|
||||
factory.f = TestQueryFactoryCancel(*args, **kw)
|
||||
return factory.f
|
||||
d = self.proxy(factory).callRemote('add', 2, 3)
|
||||
self.assertNotEqual(factory.f.connector.state, "disconnected")
|
||||
d.cancel()
|
||||
self.assertEqual(factory.f.connector.state, "disconnected")
|
||||
d = self.assertFailure(d, defer.CancelledError)
|
||||
return d
|
||||
|
||||
|
||||
def test_errorGet(self):
|
||||
"""
|
||||
A classic GET on the xml server should return a NOT_ALLOWED.
|
||||
"""
|
||||
agent = client.Agent(reactor)
|
||||
d = agent.request(b"GET", networkString("http://127.0.0.1:%d/" % (self.port,)))
|
||||
def checkResponse(response):
|
||||
self.assertEqual(response.code, http.NOT_ALLOWED)
|
||||
d.addCallback(checkResponse)
|
||||
return d
|
||||
|
||||
def test_errorXMLContent(self):
|
||||
"""
|
||||
Test that an invalid XML input returns an L{xmlrpc.Fault}.
|
||||
"""
|
||||
agent = client.Agent(reactor)
|
||||
d = agent.request(
|
||||
uri=networkString("http://127.0.0.1:%d/" % (self.port,)),
|
||||
method=b"POST",
|
||||
bodyProducer=client.FileBodyProducer(BytesIO(b"foo")))
|
||||
d.addCallback(client.readBody)
|
||||
def cb(result):
|
||||
self.assertRaises(xmlrpc.Fault, xmlrpclib.loads, result)
|
||||
d.addCallback(cb)
|
||||
return d
|
||||
|
||||
|
||||
def test_datetimeRoundtrip(self):
|
||||
|
||||
"""
|
||||
If an L{xmlrpclib.DateTime} is passed as an argument to an XML-RPC
|
||||
call and then returned by the server unmodified, the result should
|
||||
be equal to the original object.
|
||||
"""
|
||||
when = xmlrpclib.DateTime()
|
||||
d = self.proxy().callRemote("echo", when)
|
||||
d.addCallback(self.assertEqual, when)
|
||||
return d
|
||||
|
||||
|
||||
def test_doubleEncodingError(self):
|
||||
"""
|
||||
If it is not possible to encode a response to the request (for example,
|
||||
because L{xmlrpclib.dumps} raises an exception when encoding a
|
||||
L{Fault}) the exception which prevents the response from being
|
||||
generated is logged and the request object is finished anyway.
|
||||
"""
|
||||
logObserver = EventLoggingObserver()
|
||||
filtered = FilteringLogObserver(
|
||||
logObserver,
|
||||
[LogLevelFilterPredicate(defaultLogLevel=LogLevel.critical)]
|
||||
)
|
||||
globalLogPublisher.addObserver(filtered)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(filtered))
|
||||
d = self.proxy().callRemote("echo", "")
|
||||
|
||||
# *Now* break xmlrpclib.dumps. Hopefully the client already used it.
|
||||
def fakeDumps(*args, **kwargs):
|
||||
raise RuntimeError("Cannot encode anything at all!")
|
||||
self.patch(xmlrpclib, 'dumps', fakeDumps)
|
||||
|
||||
# It doesn't matter how it fails, so long as it does. Also, it happens
|
||||
# to fail with an implementation detail exception right now, not
|
||||
# something suitable as part of a public interface.
|
||||
d = self.assertFailure(d, Exception)
|
||||
|
||||
def cbFailed(ignored):
|
||||
# The fakeDumps exception should have been logged.
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIsInstance(
|
||||
logObserver[0]["log_failure"].value,
|
||||
RuntimeError
|
||||
)
|
||||
self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
|
||||
d.addCallback(cbFailed)
|
||||
return d
|
||||
|
||||
|
||||
def test_closeConnectionAfterRequest(self):
|
||||
"""
|
||||
The connection to the web server is closed when the request is done.
|
||||
"""
|
||||
d = self.proxy().callRemote('echo', '')
|
||||
def responseDone(ignored):
|
||||
[factory] = self.factories
|
||||
self.assertFalse(factory.transport.connected)
|
||||
self.assertTrue(factory.transport.disconnected)
|
||||
return d.addCallback(responseDone)
|
||||
|
||||
|
||||
def test_tcpTimeout(self):
|
||||
"""
|
||||
For I{HTTP} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
|
||||
received for the C{connectTimeout} parameter as the C{timeout} argument
|
||||
to the underlying connectTCP call.
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
proxy = xmlrpc.Proxy(b"http://127.0.0.1:69", connectTimeout=2.0,
|
||||
reactor=reactor)
|
||||
proxy.callRemote("someMethod")
|
||||
self.assertEqual(reactor.tcpClients[0][3], 2.0)
|
||||
|
||||
|
||||
def test_sslTimeout(self):
|
||||
"""
|
||||
For I{HTTPS} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
|
||||
received for the C{connectTimeout} parameter as the C{timeout} argument
|
||||
to the underlying connectSSL call.
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
proxy = xmlrpc.Proxy(b"https://127.0.0.1:69", connectTimeout=3.0,
|
||||
reactor=reactor)
|
||||
proxy.callRemote("someMethod")
|
||||
self.assertEqual(reactor.sslClients[0][4], 3.0)
|
||||
test_sslTimeout.skip = sslSkip
|
||||
|
||||
|
||||
|
||||
class XMLRPCProxyWithoutSlashTests(XMLRPCTests):
|
||||
"""
|
||||
Test with proxy that doesn't add a slash.
|
||||
"""
|
||||
|
||||
def proxy(self, factory=None):
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d" % self.port))
|
||||
if factory is None:
|
||||
p.queryFactory = self.queryFactory
|
||||
else:
|
||||
p.queryFactory = factory
|
||||
return p
|
||||
|
||||
|
||||
|
||||
class XMLRPCPublicLookupProcedureTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{XMLRPC}'s support of subclasses which override
|
||||
C{lookupProcedure} and C{listProcedures}.
|
||||
"""
|
||||
|
||||
def createServer(self, resource):
|
||||
self.p = reactor.listenTCP(
|
||||
0, server.Site(resource), interface="127.0.0.1")
|
||||
self.addCleanup(self.p.stopListening)
|
||||
self.port = self.p.getHost().port
|
||||
self.proxy = xmlrpc.Proxy(
|
||||
networkString('http://127.0.0.1:%d' % self.port))
|
||||
|
||||
|
||||
def test_lookupProcedure(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{lookupProcedure} to find
|
||||
procedures that are not defined using a C{xmlrpc_}-prefixed method name.
|
||||
"""
|
||||
self.createServer(TestLookupProcedure())
|
||||
what = "hello"
|
||||
d = self.proxy.callRemote("echo", what)
|
||||
d.addCallback(self.assertEqual, what)
|
||||
return d
|
||||
|
||||
|
||||
def test_errors(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{lookupProcedure} to raise
|
||||
L{NoSuchFunction} to indicate that a requested method is not available
|
||||
to be called, signalling a fault to the XML-RPC client.
|
||||
"""
|
||||
self.createServer(TestLookupProcedure())
|
||||
d = self.proxy.callRemote("xxxx", "hello")
|
||||
d = self.assertFailure(d, xmlrpc.Fault)
|
||||
return d
|
||||
|
||||
|
||||
def test_listMethods(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{listProcedures} to define
|
||||
Overriding listProcedures should prevent introspection from being
|
||||
broken.
|
||||
"""
|
||||
resource = TestListProcedures()
|
||||
addIntrospection(resource)
|
||||
self.createServer(resource)
|
||||
d = self.proxy.callRemote("system.listMethods")
|
||||
def listed(procedures):
|
||||
# The list will also include other introspection procedures added by
|
||||
# addIntrospection. We just want to see "foo" from our customized
|
||||
# listProcedures.
|
||||
self.assertIn('foo', procedures)
|
||||
d.addCallback(listed)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class SerializationConfigMixin:
|
||||
"""
|
||||
Mixin which defines a couple tests which should pass when a particular flag
|
||||
is passed to L{XMLRPC}.
|
||||
|
||||
These are not meant to be exhaustive serialization tests, since L{xmlrpclib}
|
||||
does all of the actual serialization work. They are just meant to exercise
|
||||
a few codepaths to make sure we are calling into xmlrpclib correctly.
|
||||
|
||||
@ivar flagName: A C{str} giving the name of the flag which must be passed to
|
||||
L{XMLRPC} to allow the tests to pass. Subclasses should set this.
|
||||
|
||||
@ivar value: A value which the specified flag will allow the serialization
|
||||
of. Subclasses should set this.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a new XML-RPC server with C{allowNone} set to C{True}.
|
||||
"""
|
||||
kwargs = {self.flagName: True}
|
||||
self.p = reactor.listenTCP(
|
||||
0, server.Site(Test(**kwargs)), interface="127.0.0.1")
|
||||
self.addCleanup(self.p.stopListening)
|
||||
self.port = self.p.getHost().port
|
||||
self.proxy = xmlrpc.Proxy(
|
||||
networkString("http://127.0.0.1:%d/" % (self.port,)), **kwargs)
|
||||
|
||||
|
||||
def test_roundtripValue(self):
|
||||
"""
|
||||
C{self.value} can be round-tripped over an XMLRPC method call/response.
|
||||
"""
|
||||
d = self.proxy.callRemote('defer', self.value)
|
||||
d.addCallback(self.assertEqual, self.value)
|
||||
return d
|
||||
|
||||
|
||||
def test_roundtripNestedValue(self):
|
||||
"""
|
||||
A C{dict} which contains C{self.value} can be round-tripped over an
|
||||
XMLRPC method call/response.
|
||||
"""
|
||||
d = self.proxy.callRemote('defer', {'a': self.value})
|
||||
d.addCallback(self.assertEqual, {'a': self.value})
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class XMLRPCAllowNoneTests(SerializationConfigMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for passing L{None} when the C{allowNone} flag is set.
|
||||
"""
|
||||
flagName = "allowNone"
|
||||
value = None
|
||||
|
||||
|
||||
class XMLRPCUseDateTimeTests(SerializationConfigMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for passing a C{datetime.datetime} instance when the C{useDateTime}
|
||||
flag is set.
|
||||
"""
|
||||
flagName = "useDateTime"
|
||||
value = datetime.datetime(2000, 12, 28, 3, 45, 59)
|
||||
|
||||
|
||||
class XMLRPCAuthenticatedTests(XMLRPCTests):
|
||||
"""
|
||||
Test with authenticated proxy. We run this with the same input/output as
|
||||
above.
|
||||
"""
|
||||
user = b"username"
|
||||
password = b"asecret"
|
||||
|
||||
def setUp(self):
|
||||
self.p = reactor.listenTCP(0, server.Site(TestAuthHeader()),
|
||||
interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
|
||||
def test_authInfoInURL(self):
|
||||
url = "http://%s:%s@127.0.0.1:%d/" % (
|
||||
nativeString(self.user), nativeString(self.password), self.port)
|
||||
p = xmlrpc.Proxy(networkString(url))
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
def test_explicitAuthInfo(self):
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, self.password)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
def test_longPassword(self):
|
||||
"""
|
||||
C{QueryProtocol} uses the C{base64.b64encode} function to encode user
|
||||
name and password in the I{Authorization} header, so that it doesn't
|
||||
embed new lines when using long inputs.
|
||||
"""
|
||||
longPassword = self.password * 40
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, longPassword)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, longPassword])
|
||||
return d
|
||||
|
||||
|
||||
def test_explicitAuthInfoOverride(self):
|
||||
p = xmlrpc.Proxy(networkString("http://wrong:info@127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, self.password)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
class XMLRPCIntrospectionTests(XMLRPCTests):
|
||||
|
||||
def setUp(self):
|
||||
xmlrpc = Test()
|
||||
addIntrospection(xmlrpc)
|
||||
self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
def test_listMethods(self):
|
||||
|
||||
def cbMethods(meths):
|
||||
meths.sort()
|
||||
self.assertEqual(
|
||||
meths,
|
||||
['add', 'complex', 'defer', 'deferFail',
|
||||
'deferFault', 'dict', 'echo', 'fail', 'fault',
|
||||
'pair', 'snowman', 'system.listMethods',
|
||||
'system.methodHelp',
|
||||
'system.methodSignature', 'withRequest'])
|
||||
|
||||
d = self.proxy().callRemote("system.listMethods")
|
||||
d.addCallback(cbMethods)
|
||||
return d
|
||||
|
||||
def test_methodHelp(self):
|
||||
inputOutputs = [
|
||||
("defer", "Help for defer."),
|
||||
("fail", ""),
|
||||
("dict", "Help for dict.")]
|
||||
|
||||
dl = []
|
||||
for meth, expected in inputOutputs:
|
||||
d = self.proxy().callRemote("system.methodHelp", meth)
|
||||
d.addCallback(self.assertEqual, expected)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
def test_methodSignature(self):
|
||||
inputOutputs = [
|
||||
("defer", ""),
|
||||
("add", [['int', 'int', 'int'],
|
||||
['double', 'double', 'double']]),
|
||||
("pair", [['array', 'string', 'int']])]
|
||||
|
||||
dl = []
|
||||
for meth, expected in inputOutputs:
|
||||
d = self.proxy().callRemote("system.methodSignature", meth)
|
||||
d.addCallback(self.assertEqual, expected)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
|
||||
class XMLRPCClientErrorHandlingTests(unittest.TestCase):
|
||||
"""
|
||||
Test error handling on the xmlrpc client.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.resource = static.Data(
|
||||
b"This text is not a valid XML-RPC response.",
|
||||
b"text/plain")
|
||||
self.resource.isLeaf = True
|
||||
|
||||
self.port = reactor.listenTCP(0, server.Site(self.resource),
|
||||
interface='127.0.0.1')
|
||||
|
||||
def tearDown(self):
|
||||
return self.port.stopListening()
|
||||
|
||||
def test_erroneousResponse(self):
|
||||
"""
|
||||
Test that calling the xmlrpc client on a static http server raises
|
||||
an exception.
|
||||
"""
|
||||
proxy = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" %
|
||||
(self.port.getHost().port,)))
|
||||
return self.assertFailure(proxy.callRemote("someMethod"), ValueError)
|
||||
|
||||
|
||||
|
||||
class QueryFactoryParseResponseTests(unittest.TestCase):
|
||||
"""
|
||||
Test the behaviour of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# The _QueryFactory that we are testing. We don't care about any
|
||||
# of the constructor parameters.
|
||||
self.queryFactory = _QueryFactory(
|
||||
path=None, host=None, method='POST', user=None, password=None,
|
||||
allowNone=False, args=())
|
||||
# An XML-RPC response that will parse without raising an error.
|
||||
self.goodContents = xmlrpclib.dumps(('',))
|
||||
# An 'XML-RPC response' that will raise a parsing error.
|
||||
self.badContents = 'invalid xml'
|
||||
# A dummy 'reason' to pass to clientConnectionLost. We don't care
|
||||
# what it is.
|
||||
self.reason = failure.Failure(ConnectionDone())
|
||||
|
||||
|
||||
def test_parseResponseCallbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as a callback
|
||||
of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addCallback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.parseResponse(self.goodContents)
|
||||
return d
|
||||
|
||||
|
||||
def test_parseResponseErrbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
|
||||
of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.parseResponse(self.badContents)
|
||||
return d
|
||||
|
||||
|
||||
def test_badStatusErrbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
|
||||
of L{_QueryFactory.badStatus}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.badStatus('status', 'message')
|
||||
return d
|
||||
|
||||
def test_parseResponseWithoutData(self):
|
||||
"""
|
||||
Some server can send a response without any data:
|
||||
L{_QueryFactory.parseResponse} should catch the error and call the
|
||||
result errback.
|
||||
"""
|
||||
content = """
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param>
|
||||
</param>
|
||||
</params>
|
||||
</methodResponse>"""
|
||||
d = self.queryFactory.deferred
|
||||
self.queryFactory.parseResponse(content)
|
||||
return self.assertFailure(d, IndexError)
|
||||
|
||||
|
||||
|
||||
class XMLRPCWithRequestTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.resource = Test()
|
||||
|
||||
|
||||
def test_withRequest(self):
|
||||
"""
|
||||
When an XML-RPC method is called and the implementation is
|
||||
decorated with L{withRequest}, the request object is passed as
|
||||
the first argument.
|
||||
"""
|
||||
request = DummyRequest('/RPC2')
|
||||
request.method = "POST"
|
||||
request.content = NativeStringIO(xmlrpclib.dumps(
|
||||
("foo",), 'withRequest'))
|
||||
def valid(n, request):
|
||||
data = xmlrpclib.loads(request.written[0])
|
||||
self.assertEqual(data, (('POST foo',), None))
|
||||
d = request.notifyFinish().addCallback(valid, request)
|
||||
self.resource.render_POST(request)
|
||||
return d
|
||||
Reference in New Issue
Block a user