This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View File

@@ -0,0 +1,6 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web}.
"""

View File

@@ -0,0 +1,95 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
General helpers for L{twisted.web} unit tests.
"""
from __future__ import annotations
from typing import Type
from twisted.internet.defer import Deferred, succeed
from twisted.trial.unittest import SynchronousTestCase
from twisted.web import server
from twisted.web._flatten import flattenString
from twisted.web.error import FlattenerError
from twisted.web.http import Request
from twisted.web.resource import IResource
from twisted.web.template import Flattenable
from .requesthelper import DummyRequest
def _render(resource: IResource, request: Request | DummyRequest) -> Deferred[None]:
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(f"Unexpected return value: {result!r}")
class FlattenTestCase(SynchronousTestCase):
"""
A test case that assists with testing L{twisted.web._flatten}.
"""
def assertFlattensTo(self, root: Flattenable, target: bytes) -> Deferred[bytes]:
"""
Assert that a root element, when flattened, is equal to a string.
"""
def check(result: bytes) -> bytes:
self.assertEqual(result, target)
return result
d: Deferred[bytes] = flattenString(None, root)
d.addCallback(check)
return d
def assertFlattensImmediately(self, root: Flattenable, target: bytes) -> bytes:
"""
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}
"""
return self.successResultOf(self.assertFlattensTo(root, target))
def assertFlatteningRaises(self, root: Flattenable, exn: Type[Exception]) -> None:
"""
Assert flattening a root element raises a particular exception.
"""
failure = self.failureResultOf(self.assertFlattensTo(root, b""), FlattenerError)
self.assertIsInstance(failure.value._exception, exn)
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"]

View File

@@ -0,0 +1,155 @@
"""
Helpers for URI and method injection tests.
@see: U{CVE-2019-12387}
"""
import string
UNPRINTABLE_ASCII = frozenset(range(0, 128)) - frozenset(
bytearray(string.printable, "ascii")
)
NONASCII = frozenset(range(128, 256))
class MethodInjectionTestsMixin:
"""
A mixin that runs HTTP method injection tests. Define
L{MethodInjectionTestsMixin.attemptRequestWithMaliciousMethod} in
a L{twisted.trial.unittest.SynchronousTestCase} subclass to test
how HTTP client code behaves when presented with malicious HTTP
methods.
@see: U{CVE-2019-12387}
"""
def attemptRequestWithMaliciousMethod(self, method):
"""
Attempt to send a request with the given method. This should
synchronously raise a L{ValueError} if either is invalid.
@param method: the method (e.g. C{GET\x00})
@param uri: the URI
@type method:
"""
raise NotImplementedError()
def test_methodWithCLRFRejected(self):
"""
Issuing a request with a method that contains a carriage
return and line feed fails with a L{ValueError}.
"""
with self.assertRaises(ValueError) as cm:
method = b"GET\r\nX-Injected-Header: value"
self.attemptRequestWithMaliciousMethod(method)
self.assertRegex(str(cm.exception), "^Invalid method")
def test_methodWithUnprintableASCIIRejected(self):
"""
Issuing a request with a method that contains unprintable
ASCII characters fails with a L{ValueError}.
"""
for c in UNPRINTABLE_ASCII:
method = b"GET%s" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousMethod(method)
self.assertRegex(str(cm.exception), "^Invalid method")
def test_methodWithNonASCIIRejected(self):
"""
Issuing a request with a method that contains non-ASCII
characters fails with a L{ValueError}.
"""
for c in NONASCII:
method = b"GET%s" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousMethod(method)
self.assertRegex(str(cm.exception), "^Invalid method")
class URIInjectionTestsMixin:
"""
A mixin that runs HTTP URI injection tests. Define
L{MethodInjectionTestsMixin.attemptRequestWithMaliciousURI} in a
L{twisted.trial.unittest.SynchronousTestCase} subclass to test how
HTTP client code behaves when presented with malicious HTTP
URIs.
"""
def attemptRequestWithMaliciousURI(self, method):
"""
Attempt to send a request with the given URI. This should
synchronously raise a L{ValueError} if either is invalid.
@param uri: the URI.
@type method:
"""
raise NotImplementedError()
def test_hostWithCRLFRejected(self):
"""
Issuing a request with a URI whose host contains a carriage
return and line feed fails with a L{ValueError}.
"""
with self.assertRaises(ValueError) as cm:
uri = b"http://twisted\r\n.invalid/path"
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")
def test_hostWithWithUnprintableASCIIRejected(self):
"""
Issuing a request with a URI whose host contains unprintable
ASCII characters fails with a L{ValueError}.
"""
for c in UNPRINTABLE_ASCII:
uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")
def test_hostWithNonASCIIRejected(self):
"""
Issuing a request with a URI whose host contains non-ASCII
characters fails with a L{ValueError}.
"""
for c in NONASCII:
uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")
def test_pathWithCRLFRejected(self):
"""
Issuing a request with a URI whose path contains a carriage
return and line feed fails with a L{ValueError}.
"""
with self.assertRaises(ValueError) as cm:
uri = b"http://twisted.invalid/\r\npath"
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")
def test_pathWithWithUnprintableASCIIRejected(self):
"""
Issuing a request with a URI whose path contains unprintable
ASCII characters fails with a L{ValueError}.
"""
for c in UNPRINTABLE_ASCII:
uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")
def test_pathWithNonASCIIRejected(self):
"""
Issuing a request with a URI whose path contains non-ASCII
characters fails with a L{ValueError}.
"""
for c in NONASCII:
uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),)
with self.assertRaises(ValueError) as cm:
self.attemptRequestWithMaliciousURI(uri)
self.assertRegex(str(cm.exception), "^Invalid URI")

View File

@@ -0,0 +1,516 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Helpers related to HTTP requests, used by tests.
"""
from __future__ import annotations
__all__ = ["DummyChannel", "DummyRequest"]
from io import BytesIO
from typing import Dict, List, Optional
from zope.interface import implementer, verify
from incremental import Version
from twisted.internet.address import IPv4Address, IPv6Address
from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IAddress, ISSLTransport
from twisted.internet.task import Clock
from twisted.python.deprecate import deprecated
from twisted.trial import unittest
from twisted.web._responses import FOUND
from twisted.web.http_headers import Headers
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET, Session, Site
textLinearWhitespaceComponents = [f"Foo{lw}bar" for lw in ["\r", "\n", "\r\n"]]
sanitizedText = "Foo bar"
bytesLinearWhitespaceComponents = [
component.encode("ascii") for component in textLinearWhitespaceComponents
]
sanitizedBytes = sanitizedText.encode("ascii")
@implementer(IAddress)
class NullAddress:
"""
A null implementation of L{IAddress}.
"""
class DummyChannel:
class TCP:
port = 80
disconnected = False
def __init__(self, peer=None):
if peer is None:
peer = IPv4Address("TCP", "192.168.1.1", 12344)
self._peer = peer
self.written = BytesIO()
self.producers = []
def getPeer(self):
return self._peer
def write(self, data):
if not isinstance(data, bytes):
raise TypeError(f"Can only write bytes to a transport, not {data!r}")
self.written.write(data)
def writeSequence(self, iovec):
for data in iovec:
self.write(data)
def getHost(self):
return IPv4Address("TCP", "10.0.0.1", self.port)
def registerProducer(self, producer, streaming):
self.producers.append((producer, streaming))
def unregisterProducer(self):
pass
def loseConnection(self):
self.disconnected = True
@implementer(ISSLTransport)
class SSL(TCP):
def abortConnection(self):
# ITCPTransport.abortConnection
pass
def getTcpKeepAlive(self):
# ITCPTransport.getTcpKeepAlive
pass
def getTcpNoDelay(self):
# ITCPTransport.getTcpNoDelay
pass
def loseWriteConnection(self):
# ITCPTransport.loseWriteConnection
pass
def setTcpKeepAlive(self, enabled):
# ITCPTransport.setTcpKeepAlive
pass
def setTcpNoDelay(self, enabled):
# ITCPTransport.setTcpNoDelay
pass
def getPeerCertificate(self):
# ISSLTransport.getPeerCertificate
pass
site = Site(Resource())
def __init__(self, peer=None):
self.transport = self.TCP(peer)
def requestDone(self, request):
pass
def writeHeaders(self, version, code, reason, headers):
if isinstance(headers, Headers):
headers = [
(k, v) for (k, values) in headers.getAllRawHeaders() for v in values
]
response_line = version + b" " + code + b" " + reason + b"\r\n"
headerSequence = [response_line]
headerSequence.extend(name + b": " + value + b"\r\n" for name, value in headers)
headerSequence.append(b"\r\n")
self.transport.writeSequence(headerSequence)
def getPeer(self):
return self.transport.getPeer()
def getHost(self):
return self.transport.getHost()
def registerProducer(self, producer, streaming):
self.transport.registerProducer(producer, streaming)
def unregisterProducer(self):
self.transport.unregisterProducer()
def write(self, data):
self.transport.write(data)
def writeSequence(self, iovec):
self.transport.writeSequence(iovec)
def loseConnection(self):
self.transport.loseConnection()
def endRequest(self):
pass
def isSecure(self):
return isinstance(self.transport, self.SSL)
def abortConnection(self):
# ITCPTransport.abortConnection
pass
def getTcpKeepAlive(self):
# ITCPTransport.getTcpKeepAlive
pass
def getTcpNoDelay(self):
# ITCPTransport.getTcpNoDelay
pass
def loseWriteConnection(self):
# ITCPTransport.loseWriteConnection
pass
def setTcpKeepAlive(self):
# ITCPTransport.setTcpKeepAlive
pass
def setTcpNoDelay(self):
# ITCPTransport.setTcpNoDelay
pass
def getPeerCertificate(self):
# ISSLTransport.getPeerCertificate
pass
class DummyRequest:
"""
Represents a dummy or fake request. See L{twisted.web.server.Request}.
@ivar _finishedDeferreds: L{None} or a C{list} of L{Deferreds} which will
be called back with L{None} when C{finish} is called or which will be
errbacked if C{processingFailed} is called.
@type requestheaders: C{Headers}
@ivar requestheaders: A Headers instance that stores values for all request
headers.
@type responseHeaders: C{Headers}
@ivar responseHeaders: A Headers instance that stores values for all
response headers.
@type responseCode: C{int}
@ivar responseCode: The response code which was passed to
C{setResponseCode}.
@type written: C{list} of C{bytes}
@ivar written: The bytes which have been written to the request.
"""
uri = b"http://dummy/"
method = b"GET"
client: Optional[IAddress] = None
sitepath: List[bytes]
written: List[bytes]
prepath: List[bytes]
args: Dict[bytes, List[bytes]]
_finishedDeferreds: List[Deferred[None]]
def registerProducer(self, prod, s):
"""
Call an L{IPullProducer}'s C{resumeProducing} method in a
loop until it unregisters itself.
@param prod: The producer.
@type prod: L{IPullProducer}
@param s: Whether or not the producer is streaming.
"""
# XXX: Handle IPushProducers
self.go = 1
while self.go:
prod.resumeProducing()
def unregisterProducer(self):
self.go = 0
def __init__(
self,
postpath: list[bytes],
session: Optional[Session] = None,
client: Optional[IAddress] = None,
) -> None:
self.sitepath = []
self.written = []
self.finished = 0
self.postpath = postpath
self.prepath = []
self.session = None
self.protoSession = session or Session(site=None, uid=b"0", reactor=Clock())
self.args = {}
self.requestHeaders = Headers()
self.responseHeaders = Headers()
self.responseCode = None
self._finishedDeferreds = []
self._serverName = b"dummy"
self.clientproto = b"HTTP/1.0"
def getAllHeaders(self):
"""
Return dictionary mapping the names of all received headers to the last
value received for each.
Since this method does not return all header information,
C{self.requestHeaders.getAllRawHeaders()} may be preferred.
NOTE: This function is a direct copy of
C{twisted.web.http.Request.getAllRawHeaders}.
"""
headers = {}
for k, v in self.requestHeaders.getAllRawHeaders():
headers[k.lower()] = v[-1]
return headers
def getHeader(self, name):
"""
Retrieve the value of a request header.
@type name: C{bytes}
@param name: The name of the request header for which to retrieve the
value. Header names are compared case-insensitively.
@rtype: C{bytes} or L{None}
@return: The value of the specified request header.
"""
return self.requestHeaders.getRawHeaders(name.lower(), [None])[0]
def setHeader(self, name, value):
"""TODO: make this assert on write() if the header is content-length"""
self.responseHeaders.addRawHeader(name, value)
def getSession(self, sessionInterface=None):
if self.session:
return self.session
assert (
not self.written
), "Session cannot be requested after data has been written."
self.session = self.protoSession
return self.session
def render(self, resource):
"""
Render the given resource as a response to this request.
This implementation only handles a few of the most common behaviors of
resources. It can handle a render method that returns a string or
C{NOT_DONE_YET}. It doesn't know anything about the semantics of
request methods (eg HEAD) nor how to set any particular headers.
Basically, it's largely broken, but sufficient for some tests at least.
It should B{not} be expanded to do all the same stuff L{Request} does.
Instead, L{DummyRequest} should be phased out and L{Request} (or some
other real code factored in a different way) used.
"""
result = resource.render(self)
if result is NOT_DONE_YET:
return
self.write(result)
self.finish()
def write(self, data):
if not isinstance(data, bytes):
raise TypeError("write() only accepts bytes")
self.written.append(data)
def notifyFinish(self) -> Deferred[None]:
"""
Return a L{Deferred} which is called back with L{None} when the request
is finished. This will probably only work if you haven't called
C{finish} yet.
"""
finished: Deferred[None] = Deferred()
self._finishedDeferreds.append(finished)
return finished
def finish(self):
"""
Record that the request is finished and callback and L{Deferred}s
waiting for notification of this.
"""
self.finished = self.finished + 1
if self._finishedDeferreds is not None:
observers = self._finishedDeferreds
self._finishedDeferreds = None
for obs in observers:
obs.callback(None)
def processingFailed(self, reason):
"""
Errback and L{Deferreds} waiting for finish notification.
"""
if self._finishedDeferreds is not None:
observers = self._finishedDeferreds
self._finishedDeferreds = None
for obs in observers:
obs.errback(reason)
def addArg(self, name, value):
self.args[name] = [value]
def setResponseCode(self, code, message=None):
"""
Set the HTTP status response code, but takes care that this is called
before any data is written.
"""
assert (
not self.written
), "Response code cannot be set after data has" "been written: {}.".format(
"@@@@".join(self.written)
)
self.responseCode = code
self.responseMessage = message
def setLastModified(self, when):
assert (
not self.written
), "Last-Modified cannot be set after data has " "been written: {}.".format(
"@@@@".join(self.written)
)
def setETag(self, tag):
assert (
not self.written
), "ETag cannot be set after data has been " "written: {}.".format(
"@@@@".join(self.written)
)
@deprecated(Version("Twisted", 18, 4, 0), replacement="getClientAddress")
def getClientIP(self):
"""
Return the IPv4 address of the client which made this request, if there
is one, otherwise L{None}.
"""
if isinstance(self.client, (IPv4Address, IPv6Address)):
return self.client.host
return None
def getClientAddress(self):
"""
Return the L{IAddress} of the client that made this request.
@return: an address.
@rtype: an L{IAddress} provider.
"""
if self.client is None:
return NullAddress()
return self.client
def getRequestHostname(self):
"""
Get a dummy hostname associated to the HTTP request.
@rtype: C{bytes}
@returns: a dummy hostname
"""
return self._serverName
def getHost(self):
"""
Get a dummy transport's host.
@rtype: C{IPv4Address}
@returns: a dummy transport's host
"""
return IPv4Address("TCP", "127.0.0.1", 80)
def setHost(self, host, port, ssl=0):
"""
Change the host and port the request thinks it's using.
@type host: C{bytes}
@param host: The value to which to change the host header.
@type ssl: C{bool}
@param ssl: A flag which, if C{True}, indicates that the request is
considered secure (if C{True}, L{isSecure} will return C{True}).
"""
self._forceSSL = ssl # set first so isSecure will work
if self.isSecure():
default = 443
else:
default = 80
if port == default:
hostHeader = host
else:
hostHeader = b"%b:%d" % (host, port)
self.requestHeaders.addRawHeader(b"host", hostHeader)
def redirect(self, url):
"""
Utility function that does a redirect.
The request should have finish() called after this.
"""
self.setResponseCode(FOUND)
self.setHeader(b"location", url)
class DummyRequestTests(unittest.SynchronousTestCase):
"""
Tests for L{DummyRequest}.
"""
def test_getClientIPDeprecated(self):
"""
L{DummyRequest.getClientIP} is deprecated in favor of
L{DummyRequest.getClientAddress}
"""
request = DummyRequest([])
request.getClientIP()
warnings = self.flushWarnings(
offendingFunctions=[self.test_getClientIPDeprecated]
)
self.assertEqual(1, len(warnings))
[warning] = warnings
self.assertEqual(warning.get("category"), DeprecationWarning)
self.assertEqual(
warning.get("message"),
(
"twisted.web.test.requesthelper.DummyRequest.getClientIP "
"was deprecated in Twisted 18.4.0; "
"please use getClientAddress instead"
),
)
def test_getClientIPSupportsIPv6(self):
"""
L{DummyRequest.getClientIP} supports IPv6 addresses, just like
L{twisted.web.http.Request.getClientIP}.
"""
request = DummyRequest([])
client = IPv6Address("TCP", "::1", 12345)
request.client = client
self.assertEqual("::1", request.getClientIP())
def test_getClientAddressWithoutClient(self):
"""
L{DummyRequest.getClientAddress} returns an L{IAddress}
provider no C{client} has been set.
"""
request = DummyRequest([])
null = request.getClientAddress()
verify.verifyObject(IAddress, null)
def test_getClientAddress(self):
"""
L{DummyRequest.getClientAddress} returns the C{client}.
"""
request = DummyRequest([])
client = IPv4Address("TCP", "127.0.0.1", 12345)
request.client = client
address = request.getClientAddress()
self.assertIs(address, client)

View File

@@ -0,0 +1,114 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web._abnf}.
"""
from twisted.trial import unittest
from twisted.web._abnf import _decint, _hexint, _ishexdigits, _istoken
class IsTokenTests(unittest.SynchronousTestCase):
"""
Test the L{twisted.web._abnf._istoken} function.
"""
def test_ok(self) -> None:
for b in (
b"GET",
b"Cache-Control",
b"&",
):
self.assertTrue(_istoken(b))
def test_bad(self) -> None:
for b in (
b"",
b" ",
b"a b",
):
self.assertFalse(_istoken(b))
class DecintTests(unittest.SynchronousTestCase):
"""
Test the L{twisted.web._abnf._decint} function.
"""
def test_valid(self) -> None:
"""
Given a decimal digits, L{_decint} return an L{int}.
"""
self.assertEqual(1, _decint(b"1"))
self.assertEqual(10, _decint(b"10"))
self.assertEqual(9000, _decint(b"9000"))
self.assertEqual(9000, _decint(b"0009000"))
def test_validWhitespace(self) -> None:
"""
L{_decint} decodes integers embedded in linear whitespace.
"""
self.assertEqual(123, _decint(b" 123"))
self.assertEqual(123, _decint(b"123\t\t"))
self.assertEqual(123, _decint(b" \t 123 \t "))
def test_invalidPlus(self) -> None:
"""
L{_decint} rejects a number with a leading C{+} character.
"""
self.assertRaises(ValueError, _decint, b"+1")
def test_invalidMinus(self) -> None:
"""
L{_decint} rejects a number with a leading C{-} character.
"""
self.assertRaises(ValueError, _decint, b"-1")
def test_invalidWhitespace(self) -> None:
"""
L{_decint} rejects a number embedded in non-linear whitespace.
"""
self.assertRaises(ValueError, _decint, b"\v1")
self.assertRaises(ValueError, _decint, b"\x1c1")
self.assertRaises(ValueError, _decint, b"1\x1e")
class HexHelperTests(unittest.SynchronousTestCase):
"""
Test the L{twisted.web._abnf._hexint} and L{_ishexdigits} helper functions.
"""
badStrings = (b"", b"0x1234", b"feds", b"-123" b"+123")
def test_isHex(self) -> None:
"""
L{_ishexdigits()} returns L{True} for nonempy bytestrings containing
hexadecimal digits.
"""
for s in (b"10", b"abcdef", b"AB1234", b"fed", b"123467890"):
self.assertIs(True, _ishexdigits(s))
def test_decodes(self) -> None:
"""
L{_hexint()} returns the integer equivalent of the input.
"""
self.assertEqual(10, _hexint(b"a"))
self.assertEqual(0x10, _hexint(b"10"))
self.assertEqual(0xABCD123, _hexint(b"abCD123"))
def test_isNotHex(self) -> None:
"""
L{_ishexdigits()} returns L{False} for bytestrings that don't contain
hexadecimal digits, including the empty string.
"""
for s in self.badStrings:
self.assertIs(False, _ishexdigits(s))
def test_decodeNotHex(self) -> None:
"""
L{_hexint()} raises L{ValueError} for bytestrings that can't
be decoded.
"""
for s in self.badStrings:
self.assertRaises(ValueError, _hexint, s)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,499 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.twcgi}.
"""
import json
import os
import sys
from io import BytesIO
from twisted.internet import address, error, interfaces, reactor
from twisted.internet.error import ConnectionLost
from twisted.python import failure, log, util
from twisted.trial import unittest
from twisted.web import client, http, http_headers, resource, server, twcgi
from twisted.web.http import INTERNAL_SERVER_ERROR, NOT_FOUND
from twisted.web.test._util import _render
from twisted.web.test.requesthelper import DummyChannel, DummyRequest
DUMMY_CGI = """\
print("Header: OK")
print("")
print("cgi output")
"""
DUAL_HEADER_CGI = """\
print("Header: spam")
print("Header: eggs")
print("")
print("cgi output")
"""
BROKEN_HEADER_CGI = """\
print("XYZ")
print("")
print("cgi output")
"""
SPECIAL_HEADER_CGI = """\
print("Server: monkeys")
print("Date: last year")
print("")
print("cgi output")
"""
READINPUT_CGI = """\
# This is an example of a correctly-written CGI script which reads a body
# from stdin, which only reads env['CONTENT_LENGTH'] bytes.
import os, sys
body_length = int(os.environ.get('CONTENT_LENGTH',0))
indata = sys.stdin.read(body_length)
print("Header: OK")
print("")
print("readinput ok")
"""
READALLINPUT_CGI = """\
# This is an example of the typical (incorrect) CGI script which expects
# the server to close stdin when the body of the request is complete.
# A correct CGI should only read env['CONTENT_LENGTH'] bytes.
import sys
indata = sys.stdin.read()
print("Header: OK")
print("")
print("readallinput ok")
"""
NO_DUPLICATE_CONTENT_TYPE_HEADER_CGI = """\
print("content-type: text/cgi-duplicate-test")
print("")
print("cgi output")
"""
HEADER_OUTPUT_CGI = """\
import json
import os
print("")
print("")
vals = {x:y for x,y in os.environ.items() if x.startswith("HTTP_")}
print(json.dumps(vals))
"""
URL_PARAMETER_CGI = """\
import os
param = str(os.environ['QUERY_STRING'])
print("Header: OK")
print("")
print(param)
"""
class PythonScript(twcgi.FilteredScript):
filter = sys.executable
class _StartServerAndTearDownMixin:
def startServer(self, cgi):
root = resource.Resource()
cgipath = util.sibpath(__file__, cgi)
root.putChild(b"cgi", PythonScript(cgipath))
site = server.Site(root)
self.p = reactor.listenTCP(0, site)
return self.p.getHost().port
def tearDown(self):
if getattr(self, "p", None):
return self.p.stopListening()
def writeCGI(self, source):
cgiFilename = os.path.abspath(self.mktemp())
with open(cgiFilename, "wt") as cgiFile:
cgiFile.write(source)
return cgiFilename
class CGITests(_StartServerAndTearDownMixin, unittest.TestCase):
"""
Tests for L{twcgi.FilteredScript}.
"""
if not interfaces.IReactorProcess.providedBy(reactor):
skip = "CGI tests require a functional reactor.spawnProcess()"
def test_CGI(self):
cgiFilename = self.writeCGI(DUMMY_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
d = client.Agent(reactor).request(b"GET", url)
d.addCallback(client.readBody)
d.addCallback(self._testCGI_1)
return d
def _testCGI_1(self, res):
self.assertEqual(res, b"cgi output" + os.linesep.encode("ascii"))
def test_protectedServerAndDate(self):
"""
If the CGI script emits a I{Server} or I{Date} header, these are
ignored.
"""
cgiFilename = self.writeCGI(SPECIAL_HEADER_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
agent = client.Agent(reactor)
d = agent.request(b"GET", url)
d.addCallback(discardBody)
def checkResponse(response):
self.assertNotIn("monkeys", response.headers.getRawHeaders("server"))
self.assertNotIn("last year", response.headers.getRawHeaders("date"))
d.addCallback(checkResponse)
return d
def test_noDuplicateContentTypeHeaders(self):
"""
If the CGI script emits a I{content-type} header, make sure that the
server doesn't add an additional (duplicate) one, as per ticket 4786.
"""
cgiFilename = self.writeCGI(NO_DUPLICATE_CONTENT_TYPE_HEADER_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
agent = client.Agent(reactor)
d = agent.request(b"GET", url)
d.addCallback(discardBody)
def checkResponse(response):
self.assertEqual(
response.headers.getRawHeaders("content-type"),
["text/cgi-duplicate-test"],
)
return response
d.addCallback(checkResponse)
return d
def test_noProxyPassthrough(self):
"""
The CGI script is never called with the Proxy header passed through.
"""
cgiFilename = self.writeCGI(HEADER_OUTPUT_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
agent = client.Agent(reactor)
headers = http_headers.Headers(
{b"Proxy": [b"foo"], b"X-Innocent-Header": [b"bar"]}
)
d = agent.request(b"GET", url, headers=headers)
def checkResponse(response):
headers = json.loads(response.decode("ascii"))
self.assertEqual(
set(headers.keys()),
{"HTTP_HOST", "HTTP_CONNECTION", "HTTP_X_INNOCENT_HEADER"},
)
d.addCallback(client.readBody)
d.addCallback(checkResponse)
return d
def test_duplicateHeaderCGI(self):
"""
If a CGI script emits two instances of the same header, both are sent
in the response.
"""
cgiFilename = self.writeCGI(DUAL_HEADER_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
agent = client.Agent(reactor)
d = agent.request(b"GET", url)
d.addCallback(discardBody)
def checkResponse(response):
self.assertEqual(response.headers.getRawHeaders("header"), ["spam", "eggs"])
d.addCallback(checkResponse)
return d
def test_malformedHeaderCGI(self):
"""
Check for the error message in the duplicated header
"""
cgiFilename = self.writeCGI(BROKEN_HEADER_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
agent = client.Agent(reactor)
d = agent.request(b"GET", url)
d.addCallback(discardBody)
loggedMessages = []
def addMessage(eventDict):
loggedMessages.append(log.textFromEventDict(eventDict))
log.addObserver(addMessage)
self.addCleanup(log.removeObserver, addMessage)
def checkResponse(ignored):
self.assertIn(
"ignoring malformed CGI header: " + repr(b"XYZ"), loggedMessages
)
d.addCallback(checkResponse)
return d
def test_ReadEmptyInput(self):
cgiFilename = os.path.abspath(self.mktemp())
with open(cgiFilename, "wt") as cgiFile:
cgiFile.write(READINPUT_CGI)
portnum = self.startServer(cgiFilename)
agent = client.Agent(reactor)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
d = agent.request(b"GET", url)
d.addCallback(client.readBody)
d.addCallback(self._test_ReadEmptyInput_1)
return d
test_ReadEmptyInput.timeout = 5 # type: ignore[attr-defined]
def _test_ReadEmptyInput_1(self, res):
expected = f"readinput ok{os.linesep}"
expected = expected.encode("ascii")
self.assertEqual(res, expected)
def test_ReadInput(self):
cgiFilename = os.path.abspath(self.mktemp())
with open(cgiFilename, "wt") as cgiFile:
cgiFile.write(READINPUT_CGI)
portnum = self.startServer(cgiFilename)
agent = client.Agent(reactor)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
d = agent.request(
uri=url,
method=b"POST",
bodyProducer=client.FileBodyProducer(BytesIO(b"Here is your stdin")),
)
d.addCallback(client.readBody)
d.addCallback(self._test_ReadInput_1)
return d
test_ReadInput.timeout = 5 # type: ignore[attr-defined]
def _test_ReadInput_1(self, res):
expected = f"readinput ok{os.linesep}"
expected = expected.encode("ascii")
self.assertEqual(res, expected)
def test_ReadAllInput(self):
cgiFilename = os.path.abspath(self.mktemp())
with open(cgiFilename, "wt") as cgiFile:
cgiFile.write(READALLINPUT_CGI)
portnum = self.startServer(cgiFilename)
url = "http://localhost:%d/cgi" % (portnum,)
url = url.encode("ascii")
d = client.Agent(reactor).request(
uri=url,
method=b"POST",
bodyProducer=client.FileBodyProducer(BytesIO(b"Here is your stdin")),
)
d.addCallback(client.readBody)
d.addCallback(self._test_ReadAllInput_1)
return d
test_ReadAllInput.timeout = 5 # type: ignore[attr-defined]
def _test_ReadAllInput_1(self, res):
expected = f"readallinput ok{os.linesep}"
expected = expected.encode("ascii")
self.assertEqual(res, expected)
def test_useReactorArgument(self):
"""
L{twcgi.FilteredScript.runProcess} uses the reactor passed as an
argument to the constructor.
"""
class FakeReactor:
"""
A fake reactor recording whether spawnProcess is called.
"""
called = False
def spawnProcess(self, *args, **kwargs):
"""
Set the C{called} flag to C{True} if C{spawnProcess} is called.
@param args: Positional arguments.
@param kwargs: Keyword arguments.
"""
self.called = True
fakeReactor = FakeReactor()
request = DummyRequest(["a", "b"])
request.client = address.IPv4Address("TCP", "127.0.0.1", 12345)
resource = twcgi.FilteredScript("dummy-file", reactor=fakeReactor)
_render(resource, request)
self.assertTrue(fakeReactor.called)
class CGIScriptTests(_StartServerAndTearDownMixin, unittest.TestCase):
"""
Tests for L{twcgi.CGIScript}.
"""
def test_urlParameters(self):
"""
If the CGI script is passed URL parameters, do not fall over,
as per ticket 9887.
"""
cgiFilename = self.writeCGI(URL_PARAMETER_CGI)
portnum = self.startServer(cgiFilename)
url = b"http://localhost:%d/cgi?param=1234" % (portnum,)
agent = client.Agent(reactor)
d = agent.request(b"GET", url)
d.addCallback(client.readBody)
d.addCallback(self._test_urlParameters_1)
return d
def _test_urlParameters_1(self, res):
expected = f"param=1234{os.linesep}"
expected = expected.encode("ascii")
self.assertEqual(res, expected)
def test_pathInfo(self):
"""
L{twcgi.CGIScript.render} sets the process environment
I{PATH_INFO} from the request path.
"""
class FakeReactor:
"""
A fake reactor recording the environment passed to spawnProcess.
"""
def spawnProcess(self, process, filename, args, env, wdir):
"""
Store the C{env} L{dict} to an instance attribute.
@param process: Ignored
@param filename: Ignored
@param args: Ignored
@param env: The environment L{dict} which will be stored
@param wdir: Ignored
"""
self.process_env = env
_reactor = FakeReactor()
resource = twcgi.CGIScript(self.mktemp(), reactor=_reactor)
request = DummyRequest(["a", "b"])
request.client = address.IPv4Address("TCP", "127.0.0.1", 12345)
_render(resource, request)
self.assertEqual(_reactor.process_env["PATH_INFO"], "/a/b")
class CGIDirectoryTests(unittest.TestCase):
"""
Tests for L{twcgi.CGIDirectory}.
"""
def test_render(self):
"""
L{twcgi.CGIDirectory.render} sets the HTTP response code to I{NOT
FOUND}.
"""
resource = twcgi.CGIDirectory(self.mktemp())
request = DummyRequest([""])
d = _render(resource, request)
def cbRendered(ignored):
self.assertEqual(request.responseCode, NOT_FOUND)
d.addCallback(cbRendered)
return d
def test_notFoundChild(self):
"""
L{twcgi.CGIDirectory.getChild} returns a resource which renders an
response with the HTTP I{NOT FOUND} status code if the indicated child
does not exist as an entry in the directory used to initialized the
L{twcgi.CGIDirectory}.
"""
path = self.mktemp()
os.makedirs(path)
resource = twcgi.CGIDirectory(path)
request = DummyRequest(["foo"])
child = resource.getChild("foo", request)
d = _render(child, request)
def cbRendered(ignored):
self.assertEqual(request.responseCode, NOT_FOUND)
d.addCallback(cbRendered)
return d
class CGIProcessProtocolTests(unittest.TestCase):
"""
Tests for L{twcgi.CGIProcessProtocol}.
"""
def test_prematureEndOfHeaders(self):
"""
If the process communicating with L{CGIProcessProtocol} ends before
finishing writing out headers, the response has I{INTERNAL SERVER
ERROR} as its status code.
"""
request = DummyRequest([""])
protocol = twcgi.CGIProcessProtocol(request)
protocol.processEnded(failure.Failure(error.ProcessTerminated()))
self.assertEqual(request.responseCode, INTERNAL_SERVER_ERROR)
def test_connectionLost(self):
"""
Ensure that the CGI process ends cleanly when the request connection
is lost.
"""
d = DummyChannel()
request = http.Request(d, True)
protocol = twcgi.CGIProcessProtocol(request)
request.connectionLost(failure.Failure(ConnectionLost("Connection done")))
protocol.processEnded(failure.Failure(error.ProcessTerminated()))
def discardBody(response):
"""
Discard the body of a HTTP response.
@param response: The response.
@return: The response.
"""
return client.readBody(response).addCallback(lambda _: response)

View File

@@ -0,0 +1,50 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for various parts of L{twisted.web}.
"""
from __future__ import annotations
from zope.interface import implementer, verify
from twisted.internet import defer, interfaces
from twisted.trial import unittest
from twisted.web import client
@implementer(interfaces.IStreamClientEndpoint)
class DummyEndPoint:
"""An endpoint that does not connect anywhere"""
def __init__(self, someString: str) -> None:
self.someString = someString
def __repr__(self) -> str:
return f"DummyEndPoint({self.someString})"
def connect( # type: ignore[override]
self, factory: interfaces.IProtocolFactory
) -> defer.Deferred[dict[str, interfaces.IProtocolFactory]]:
return defer.succeed(dict(factory=factory))
class HTTPConnectionPoolTests(unittest.TestCase):
"""
Unit tests for L{client.HTTPConnectionPoolTest}.
"""
def test_implements(self) -> None:
"""L{DummyEndPoint}s implements L{interfaces.IStreamClientEndpoint}"""
ep = DummyEndPoint("something")
verify.verifyObject(interfaces.IStreamClientEndpoint, ep)
def test_repr(self) -> None:
"""connection L{repr()} includes endpoint's L{repr()}"""
pool = client.HTTPConnectionPool(reactor=None)
ep = DummyEndPoint("this_is_probably_unique")
d = pool.getConnection("someplace", ep)
result = self.successResultOf(d)
representation = repr(result)
self.assertIn(repr(ep), representation)

View File

@@ -0,0 +1,502 @@
# 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 as _pwd
except ImportError:
pwd = None
else:
pwd = _pwd
from unittest import skipIf
from zope.interface.verify import verifyObject
from twisted.internet import defer, reactor
from twisted.logger import globalLogPublisher
from twisted.python import failure, filepath
from twisted.spread import pb
from twisted.spread.banana import SIZE_LIMIT
from twisted.test import proto_helpers
from twisted.trial.unittest import TestCase
from twisted.web import client, distrib, resource, server, static
from twisted.web.http_headers import Headers
from twisted.web.test._util import _render
from twisted.web.test.requesthelper import DummyChannel, DummyRequest
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(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 = f"http://127.0.0.1:{self.port2.getHost().port}/here/there"
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 = f"http://{mainAddr.host}:{mainAddr.port}/child"
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 = f"http://{mainAddr.host}:{mainAddr.port}/child"
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(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))
async def _404Test(self, name: bytes) -> None:
"""
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)
await d
self.assertEqual(request.responseCode, 404)
async 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.
"""
await self._404Test(b"carol")
async 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.
"""
await self._404Test(b"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(b"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(b"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
@skipIf(not pwd, "pwd module required")
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)

View File

@@ -0,0 +1,305 @@
# -*- test-case-name: twisted.web.test.test_domhelpers -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Specific tests for (some of) the methods in L{twisted.web.domhelpers}.
"""
from importlib import reload
from typing import Any, Optional
from xml.dom import minidom
from twisted.trial.unittest import TestCase
from twisted.web import domhelpers, microdom
class DOMHelpersTestsMixin:
"""
A mixin for L{TestCase} subclasses which defines test methods for
domhelpers functionality based on a DOM creation function provided by a
subclass.
"""
dom: Optional[Any] = None
def test_getElementsByTagName(self):
doc1 = self.dom.parseString("<foo/>")
actual = domhelpers.getElementsByTagName(doc1, "foo")[0].nodeName
expected = "foo"
self.assertEqual(actual, expected)
el1 = doc1.documentElement
actual = domhelpers.getElementsByTagName(el1, "foo")[0].nodeName
self.assertEqual(actual, expected)
doc2_xml = '<a><foo in="a"/><b><foo in="b"/></b><c><foo in="c"/></c><foo in="d"/><foo in="ef"/><g><foo in="g"/><h><foo in="h"/></h></g></a>'
doc2 = self.dom.parseString(doc2_xml)
tag_list = domhelpers.getElementsByTagName(doc2, "foo")
actual = "".join([node.getAttribute("in") for node in tag_list])
expected = "abcdefgh"
self.assertEqual(actual, expected)
el2 = doc2.documentElement
tag_list = domhelpers.getElementsByTagName(el2, "foo")
actual = "".join([node.getAttribute("in") for node in tag_list])
self.assertEqual(actual, expected)
doc3_xml = """
<a><foo in="a"/>
<b><foo in="b"/>
<d><foo in="d"/>
<g><foo in="g"/></g>
<h><foo in="h"/></h>
</d>
<e><foo in="e"/>
<i><foo in="i"/></i>
</e>
</b>
<c><foo in="c"/>
<f><foo in="f"/>
<j><foo in="j"/></j>
</f>
</c>
</a>"""
doc3 = self.dom.parseString(doc3_xml)
tag_list = domhelpers.getElementsByTagName(doc3, "foo")
actual = "".join([node.getAttribute("in") for node in tag_list])
expected = "abdgheicfj"
self.assertEqual(actual, expected)
el3 = doc3.documentElement
tag_list = domhelpers.getElementsByTagName(el3, "foo")
actual = "".join([node.getAttribute("in") for node in tag_list])
self.assertEqual(actual, expected)
doc4_xml = "<foo><bar></bar><baz><foo/></baz></foo>"
doc4 = self.dom.parseString(doc4_xml)
actual = domhelpers.getElementsByTagName(doc4, "foo")
root = doc4.documentElement
expected = [root, root.childNodes[-1].childNodes[0]]
self.assertEqual(actual, expected)
actual = domhelpers.getElementsByTagName(root, "foo")
self.assertEqual(actual, expected)
def test_gatherTextNodes(self):
doc1 = self.dom.parseString("<a>foo</a>")
actual = domhelpers.gatherTextNodes(doc1)
expected = "foo"
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc1.documentElement)
self.assertEqual(actual, expected)
doc2_xml = "<a>a<b>b</b><c>c</c>def<g>g<h>h</h></g></a>"
doc2 = self.dom.parseString(doc2_xml)
actual = domhelpers.gatherTextNodes(doc2)
expected = "abcdefgh"
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc2.documentElement)
self.assertEqual(actual, expected)
doc3_xml = (
"<a>a<b>b<d>d<g>g</g><h>h</h></d><e>e<i>i</i></e></b>"
+ "<c>c<f>f<j>j</j></f></c></a>"
)
doc3 = self.dom.parseString(doc3_xml)
actual = domhelpers.gatherTextNodes(doc3)
expected = "abdgheicfj"
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc3.documentElement)
self.assertEqual(actual, expected)
def test_clearNode(self):
doc1 = self.dom.parseString("<a><b><c><d/></c></b></a>")
a_node = doc1.documentElement
domhelpers.clearNode(a_node)
self.assertEqual(a_node.toxml(), self.dom.Element("a").toxml())
doc2 = self.dom.parseString("<a><b><c><d/></c></b></a>")
b_node = doc2.documentElement.childNodes[0]
domhelpers.clearNode(b_node)
actual = doc2.documentElement.toxml()
expected = self.dom.Element("a")
expected.appendChild(self.dom.Element("b"))
self.assertEqual(actual, expected.toxml())
def test_get(self):
doc1 = self.dom.parseString('<a><b id="bar"/><c class="foo"/></a>')
doc = self.dom.Document()
node = domhelpers.get(doc1, "foo")
actual = node.toxml()
expected = doc.createElement("c")
expected.setAttribute("class", "foo")
self.assertEqual(actual, expected.toxml())
node = domhelpers.get(doc1, "bar")
actual = node.toxml()
expected = doc.createElement("b")
expected.setAttribute("id", "bar")
self.assertEqual(actual, expected.toxml())
self.assertRaises(domhelpers.NodeLookupError, domhelpers.get, doc1, "pzork")
def test_getIfExists(self):
doc1 = self.dom.parseString('<a><b id="bar"/><c class="foo"/></a>')
doc = self.dom.Document()
node = domhelpers.getIfExists(doc1, "foo")
actual = node.toxml()
expected = doc.createElement("c")
expected.setAttribute("class", "foo")
self.assertEqual(actual, expected.toxml())
node = domhelpers.getIfExists(doc1, "pzork")
self.assertIdentical(node, None)
def test_getAndClear(self):
doc1 = self.dom.parseString('<a><b id="foo"><c></c></b></a>')
doc = self.dom.Document()
node = domhelpers.getAndClear(doc1, "foo")
actual = node.toxml()
expected = doc.createElement("b")
expected.setAttribute("id", "foo")
self.assertEqual(actual, expected.toxml())
def test_locateNodes(self):
doc1 = self.dom.parseString(
'<a><b foo="olive"><c foo="olive"/></b><d foo="poopy"/></a>'
)
doc = self.dom.Document()
node_list = domhelpers.locateNodes(doc1.childNodes, "foo", "olive", noNesting=1)
actual = "".join([node.toxml() for node in node_list])
expected = doc.createElement("b")
expected.setAttribute("foo", "olive")
c = doc.createElement("c")
c.setAttribute("foo", "olive")
expected.appendChild(c)
self.assertEqual(actual, expected.toxml())
node_list = domhelpers.locateNodes(doc1.childNodes, "foo", "olive", noNesting=0)
actual = "".join([node.toxml() for node in node_list])
self.assertEqual(actual, expected.toxml() + c.toxml())
def test_getParents(self):
doc1 = self.dom.parseString("<a><b><c><d/></c><e/></b><f/></a>")
node_list = domhelpers.getParents(
doc1.childNodes[0].childNodes[0].childNodes[0]
)
actual = "".join(
[node.tagName for node in node_list if hasattr(node, "tagName")]
)
self.assertEqual(actual, "cba")
def test_findElementsWithAttribute(self):
doc1 = self.dom.parseString('<a foo="1"><b foo="2"/><c foo="1"/><d/></a>')
node_list = domhelpers.findElementsWithAttribute(doc1, "foo")
actual = "".join([node.tagName for node in node_list])
self.assertEqual(actual, "abc")
node_list = domhelpers.findElementsWithAttribute(doc1, "foo", "1")
actual = "".join([node.tagName for node in node_list])
self.assertEqual(actual, "ac")
def test_findNodesNamed(self):
doc1 = self.dom.parseString("<doc><foo/><bar/><foo>a</foo></doc>")
node_list = domhelpers.findNodesNamed(doc1, "foo")
actual = len(node_list)
self.assertEqual(actual, 2)
def test_escape(self):
j = "this string \" contains many & characters> xml< won't like"
expected = (
"this string &quot; contains many &amp; characters&gt; xml&lt; won't like"
)
self.assertEqual(domhelpers.escape(j), expected)
def test_unescape(self):
j = "this string &quot; has &&amp; entities &gt; &lt; and some characters xml won't like<"
expected = (
"this string \" has && entities > < and some characters xml won't like<"
)
self.assertEqual(domhelpers.unescape(j), expected)
def test_getNodeText(self):
"""
L{getNodeText} returns the concatenation of all the text data at or
beneath the node passed to it.
"""
node = self.dom.parseString("<foo><bar>baz</bar><bar>quux</bar></foo>")
self.assertEqual(domhelpers.getNodeText(node), "bazquux")
class MicroDOMHelpersTests(DOMHelpersTestsMixin, TestCase):
dom = microdom
def test_gatherTextNodesDropsWhitespace(self):
"""
Microdom discards whitespace-only text nodes, so L{gatherTextNodes}
returns only the text from nodes which had non-whitespace characters.
"""
doc4_xml = """<html>
<head>
</head>
<body>
stuff
</body>
</html>
"""
doc4 = self.dom.parseString(doc4_xml)
actual = domhelpers.gatherTextNodes(doc4)
expected = "\n stuff\n "
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc4.documentElement)
self.assertEqual(actual, expected)
def test_textEntitiesNotDecoded(self):
"""
Microdom does not decode entities in text nodes.
"""
doc5_xml = "<x>Souffl&amp;</x>"
doc5 = self.dom.parseString(doc5_xml)
actual = domhelpers.gatherTextNodes(doc5)
expected = "Souffl&amp;"
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc5.documentElement)
self.assertEqual(actual, expected)
def test_deprecation(self):
"""
An import will raise the deprecation warning.
"""
reload(domhelpers)
warnings = self.flushWarnings([self.test_deprecation])
self.assertEqual(1, len(warnings))
self.assertEqual(
"twisted.web.domhelpers was deprecated at Twisted 23.10.0",
warnings[0]["message"],
)
class MiniDOMHelpersTests(DOMHelpersTestsMixin, TestCase):
dom = minidom
def test_textEntitiesDecoded(self):
"""
Minidom does decode entities in text nodes.
"""
doc5_xml = "<x>Souffl&amp;</x>"
doc5 = self.dom.parseString(doc5_xml)
actual = domhelpers.gatherTextNodes(doc5)
expected = "Souffl&"
self.assertEqual(actual, expected)
actual = domhelpers.gatherTextNodes(doc5.documentElement)
self.assertEqual(actual, expected)
def test_getNodeUnicodeText(self):
"""
L{domhelpers.getNodeText} returns a C{unicode} string when text
nodes are represented in the DOM with unicode, whether or not there
are non-ASCII characters present.
"""
node = self.dom.parseString("<foo>bar</foo>")
text = domhelpers.getNodeText(node)
self.assertEqual(text, "bar")
self.assertIsInstance(text, str)
node = self.dom.parseString("<foo>\N{SNOWMAN}</foo>".encode())
text = domhelpers.getNodeText(node)
self.assertEqual(text, "\N{SNOWMAN}")
self.assertIsInstance(text, str)

View File

@@ -0,0 +1,477 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
HTTP errors.
"""
from __future__ import annotations
import re
import sys
import traceback
from twisted.python.compat import nativeString
from twisted.trial import unittest
from twisted.web import error
from twisted.web.template import Tag
class CodeToMessageTests(unittest.TestCase):
"""
L{_codeToMessages} inverts L{_responses.RESPONSES}
"""
def test_validCode(self) -> None:
m = error._codeToMessage(b"302")
self.assertEqual(m, b"Found")
def test_invalidCode(self) -> None:
m = error._codeToMessage(b"987")
self.assertEqual(m, None)
def test_nonintegerCode(self) -> None:
m = error._codeToMessage(b"InvalidCode")
self.assertEqual(m, None)
class ErrorTests(unittest.TestCase):
"""
Tests for how L{Error} attributes are initialized.
"""
def test_noMessageValidStatus(self) -> None:
"""
If no C{message} argument is passed to the L{Error} constructor and the
C{code} argument is a valid HTTP status code, C{message} is set to the
HTTP reason phrase for C{code}.
"""
e = error.Error(b"200")
self.assertEqual(e.message, b"OK")
self.assertEqual(str(e), "200 OK")
def test_noMessageForStatus(self) -> None:
"""
If no C{message} argument is passed to the L{Error} constructor and
C{code} isn't a known HTTP status code, C{message} stays L{None}.
"""
e = error.Error(b"999")
self.assertEqual(e.message, None)
self.assertEqual(str(e), "999")
def test_invalidStatus(self) -> None:
"""
If C{code} isn't plausibly an HTTP status code (i.e., composed of
digits) it is rejected with L{ValueError}.
"""
with self.assertRaises(ValueError):
error.Error(b"InvalidStatus")
def test_messageExists(self) -> None:
"""
If a C{message} argument is passed to the L{Error} constructor, the
C{message} isn't affected by the value of C{status}.
"""
e = error.Error(b"200", b"My own message")
self.assertEqual(e.message, b"My own message")
self.assertEqual(str(e), "200 My own message")
def test_str(self) -> None:
"""
C{str()} on an L{Error} returns the code and message it was
instantiated with.
"""
# Bytestring status
e = error.Error(b"200", b"OK")
self.assertEqual(str(e), "200 OK")
# int status
e = error.Error(200, b"OK")
self.assertEqual(str(e), "200 OK")
class PageRedirectTests(unittest.TestCase):
"""
Tests for how L{PageRedirect} attributes are initialized.
"""
def test_noMessageValidStatus(self) -> None:
"""
If no C{message} argument is passed to the L{PageRedirect} constructor
and the C{code} argument is a valid HTTP status code, C{code} is mapped
to a descriptive string to which C{message} is assigned.
"""
e = error.PageRedirect(b"200", location=b"/foo")
self.assertEqual(e.message, b"OK to /foo")
def test_noMessageValidStatusNoLocation(self) -> None:
"""
If no C{message} argument is passed to the L{PageRedirect} constructor
and C{location} is also empty and the C{code} argument is a valid HTTP
status code, C{code} is mapped to a descriptive string to which
C{message} is assigned without trying to include an empty location.
"""
e = error.PageRedirect(b"200")
self.assertEqual(e.message, b"OK")
def test_noMessageInvalidStatusLocationExists(self) -> None:
"""
If no C{message} argument is passed to the L{PageRedirect} constructor
and C{code} isn't a valid HTTP status code, C{message} stays L{None}.
"""
e = error.PageRedirect(b"999", location=b"/foo")
self.assertEqual(e.message, None)
def test_messageExistsLocationExists(self) -> None:
"""
If a C{message} argument is passed to the L{PageRedirect} constructor,
the C{message} isn't affected by the value of C{status}.
"""
e = error.PageRedirect(b"200", b"My own message", location=b"/foo")
self.assertEqual(e.message, b"My own message to /foo")
def test_messageExistsNoLocation(self) -> None:
"""
If a C{message} argument is passed to the L{PageRedirect} constructor
and no location is provided, C{message} doesn't try to include the
empty location.
"""
e = error.PageRedirect(b"200", b"My own message")
self.assertEqual(e.message, b"My own message")
class InfiniteRedirectionTests(unittest.TestCase):
"""
Tests for how L{InfiniteRedirection} attributes are initialized.
"""
def test_noMessageValidStatus(self) -> None:
"""
If no C{message} argument is passed to the L{InfiniteRedirection}
constructor and the C{code} argument is a valid HTTP status code,
C{code} is mapped to a descriptive string to which C{message} is
assigned.
"""
e = error.InfiniteRedirection(b"200", location=b"/foo")
self.assertEqual(e.message, b"OK to /foo")
def test_noMessageValidStatusNoLocation(self) -> None:
"""
If no C{message} argument is passed to the L{InfiniteRedirection}
constructor and C{location} is also empty and the C{code} argument is a
valid HTTP status code, C{code} is mapped to a descriptive string to
which C{message} is assigned without trying to include an empty
location.
"""
e = error.InfiniteRedirection(b"200")
self.assertEqual(e.message, b"OK")
def test_noMessageInvalidStatusLocationExists(self) -> None:
"""
If no C{message} argument is passed to the L{InfiniteRedirection}
constructor and C{code} isn't a valid HTTP status code, C{message} stays
L{None}.
"""
e = error.InfiniteRedirection(b"999", location=b"/foo")
self.assertEqual(e.message, None)
self.assertEqual(str(e), "999")
def test_messageExistsLocationExists(self) -> None:
"""
If a C{message} argument is passed to the L{InfiniteRedirection}
constructor, the C{message} isn't affected by the value of C{status}.
"""
e = error.InfiniteRedirection(b"200", b"My own message", location=b"/foo")
self.assertEqual(e.message, b"My own message to /foo")
def test_messageExistsNoLocation(self) -> None:
"""
If a C{message} argument is passed to the L{InfiniteRedirection}
constructor and no location is provided, C{message} doesn't try to
include the empty location.
"""
e = error.InfiniteRedirection(b"200", b"My own message")
self.assertEqual(e.message, b"My own message")
class RedirectWithNoLocationTests(unittest.TestCase):
"""
L{RedirectWithNoLocation} is a subclass of L{Error} which sets
a custom message in the constructor.
"""
def test_validMessage(self) -> None:
"""
When C{code}, C{message}, and C{uri} are passed to the
L{RedirectWithNoLocation} constructor, the C{message} and C{uri}
attributes are set, respectively.
"""
e = error.RedirectWithNoLocation(b"302", b"REDIRECT", b"https://example.com")
self.assertEqual(e.message, b"REDIRECT to https://example.com")
self.assertEqual(e.uri, b"https://example.com")
class MissingRenderMethodTests(unittest.TestCase):
"""
Tests for how L{MissingRenderMethod} exceptions are initialized and
displayed.
"""
def test_constructor(self) -> None:
"""
Given C{element} and C{renderName} arguments, the
L{MissingRenderMethod} constructor assigns the values to the
corresponding attributes.
"""
elt = object()
e = error.MissingRenderMethod(elt, "renderThing")
self.assertIs(e.element, elt)
self.assertIs(e.renderName, "renderThing")
def test_repr(self) -> None:
"""
A L{MissingRenderMethod} is represented using a custom string
containing the element's representation and the method name.
"""
elt = object()
e = error.MissingRenderMethod(elt, "renderThing")
self.assertEqual(
repr(e),
("'MissingRenderMethod': " "%r had no render method named 'renderThing'")
% elt,
)
class MissingTemplateLoaderTests(unittest.TestCase):
"""
Tests for how L{MissingTemplateLoader} exceptions are initialized and
displayed.
"""
def test_constructor(self) -> None:
"""
Given an C{element} argument, the L{MissingTemplateLoader} constructor
assigns the value to the corresponding attribute.
"""
elt = object()
e = error.MissingTemplateLoader(elt)
self.assertIs(e.element, elt)
def test_repr(self) -> None:
"""
A L{MissingTemplateLoader} is represented using a custom string
containing the element's representation and the method name.
"""
elt = object()
e = error.MissingTemplateLoader(elt)
self.assertEqual(repr(e), "'MissingTemplateLoader': %r had no loader" % elt)
class FlattenerErrorTests(unittest.TestCase):
"""
Tests for L{FlattenerError}.
"""
def makeFlattenerError(self, roots: list[object] = []) -> error.FlattenerError:
try:
raise RuntimeError("oh noes")
except Exception as e:
tb = traceback.extract_tb(sys.exc_info()[2])
return error.FlattenerError(e, roots, tb)
def fakeFormatRoot(self, obj: object) -> str:
return "R(%s)" % obj
def test_constructor(self) -> None:
"""
Given C{exception}, C{roots}, and C{traceback} arguments, the
L{FlattenerError} constructor assigns the roots to the C{_roots}
attribute.
"""
e = self.makeFlattenerError(roots=["a", "b"])
self.assertEqual(e._roots, ["a", "b"])
def test_str(self) -> None:
"""
The string form of a L{FlattenerError} is identical to its
representation.
"""
e = self.makeFlattenerError()
self.assertEqual(str(e), repr(e))
def test_reprWithRootsAndWithTraceback(self) -> None:
"""
The representation of a L{FlattenerError} initialized with roots and a
traceback contains a formatted representation of those roots (using
C{_formatRoot}) and a formatted traceback.
"""
e = self.makeFlattenerError(["a", "b"])
e._formatRoot = self.fakeFormatRoot # type: ignore[method-assign]
self.assertTrue(
re.match(
"Exception while flattening:\n"
" R\\(a\\)\n"
" R\\(b\\)\n"
' File "[^"]*", line [0-9]*, in makeFlattenerError\n'
' raise RuntimeError\\("oh noes"\\)\n'
"RuntimeError: oh noes\n$",
repr(e),
re.M | re.S,
),
repr(e),
)
def test_reprWithoutRootsAndWithTraceback(self) -> None:
"""
The representation of a L{FlattenerError} initialized without roots but
with a traceback contains a formatted traceback but no roots.
"""
e = self.makeFlattenerError([])
self.assertTrue(
re.match(
"Exception while flattening:\n"
' File "[^"]*", line [0-9]*, in makeFlattenerError\n'
' raise RuntimeError\\("oh noes"\\)\n'
"RuntimeError: oh noes\n$",
repr(e),
re.M | re.S,
),
repr(e),
)
def test_reprWithoutRootsAndWithoutTraceback(self) -> None:
"""
The representation of a L{FlattenerError} initialized without roots but
with a traceback contains a formatted traceback but no roots.
"""
e = error.FlattenerError(RuntimeError("oh noes"), [], None)
self.assertTrue(
re.match(
"Exception while flattening:\n" "RuntimeError: oh noes\n$",
repr(e),
re.M | re.S,
),
repr(e),
)
def test_formatRootShortUnicodeString(self) -> None:
"""
The C{_formatRoot} method formats a short unicode string using the
built-in repr.
"""
e = self.makeFlattenerError()
self.assertEqual(e._formatRoot(nativeString("abcd")), repr("abcd"))
def test_formatRootLongUnicodeString(self) -> None:
"""
The C{_formatRoot} method formats a long unicode string using the
built-in repr with an ellipsis.
"""
e = self.makeFlattenerError()
longString = nativeString("abcde-" * 20)
self.assertEqual(
e._formatRoot(longString),
repr("abcde-abcde-abcde-ab<...>e-abcde-abcde-abcde-"),
)
def test_formatRootShortByteString(self) -> None:
"""
The C{_formatRoot} method formats a short byte string using the
built-in repr.
"""
e = self.makeFlattenerError()
self.assertEqual(e._formatRoot(b"abcd"), repr(b"abcd"))
def test_formatRootLongByteString(self) -> None:
"""
The C{_formatRoot} method formats a long byte string using the
built-in repr with an ellipsis.
"""
e = self.makeFlattenerError()
longString = b"abcde-" * 20
self.assertEqual(
e._formatRoot(longString),
repr(b"abcde-abcde-abcde-ab<...>e-abcde-abcde-abcde-"),
)
def test_formatRootTagNoFilename(self) -> None:
"""
The C{_formatRoot} method formats a C{Tag} with no filename information
as 'Tag <tagName>'.
"""
e = self.makeFlattenerError()
self.assertEqual(e._formatRoot(Tag("a-tag")), "Tag <a-tag>")
def test_formatRootTagWithFilename(self) -> None:
"""
The C{_formatRoot} method formats a C{Tag} with filename information
using the filename, line, column, and tag information
"""
e = self.makeFlattenerError()
t = Tag("a-tag", filename="tpl.py", lineNumber=10, columnNumber=20)
self.assertEqual(
e._formatRoot(t), 'File "tpl.py", line 10, column 20, in "a-tag"'
)
def test_string(self) -> None:
"""
If a L{FlattenerError} is created with a string root, up to around 40
bytes from that string are included in the string representation of the
exception.
"""
self.assertEqual(
str(error.FlattenerError(RuntimeError("reason"), ["abc123xyz"], [])),
"Exception while flattening:\n" " 'abc123xyz'\n" "RuntimeError: reason\n",
)
self.assertEqual(
str(error.FlattenerError(RuntimeError("reason"), ["0123456789" * 10], [])),
"Exception while flattening:\n"
" '01234567890123456789"
"<...>01234567890123456789'\n" # TODO: re-add 0
"RuntimeError: reason\n",
)
def test_unicode(self) -> None:
"""
If a L{FlattenerError} is created with a unicode root, up to around 40
characters from that string are included in the string representation
of the exception.
"""
self.assertEqual(
str(
error.FlattenerError(RuntimeError("reason"), ["abc\N{SNOWMAN}xyz"], [])
),
"Exception while flattening:\n"
" 'abc\\u2603xyz'\n" # Codepoint for SNOWMAN
"RuntimeError: reason\n",
)
self.assertEqual(
str(
error.FlattenerError(
RuntimeError("reason"), ["01234567\N{SNOWMAN}9" * 10], []
)
),
"Exception while flattening:\n"
" '01234567\\u2603901234567\\u26039"
"<...>01234567\\u2603901234567"
"\\u26039'\n"
"RuntimeError: reason\n",
)
class UnsupportedMethodTests(unittest.SynchronousTestCase):
"""
Tests for L{UnsupportedMethod}.
"""
def test_str(self) -> None:
"""
The C{__str__} for L{UnsupportedMethod} makes it clear that what it
shows is a list of the supported methods, not the method that was
unsupported.
"""
e = error.UnsupportedMethod([b"HEAD", b"PATCH"])
self.assertEqual(
str(e),
"Expected one of [b'HEAD', b'PATCH']",
)

View File

@@ -0,0 +1,767 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for the flattening portion of L{twisted.web.template}, implemented in
L{twisted.web._flatten}.
"""
import re
import sys
import traceback
from collections import OrderedDict
from textwrap import dedent
from types import FunctionType
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
from xml.etree.ElementTree import XML
from zope.interface import implementer
from hamcrest import assert_that, equal_to
from twisted.internet.defer import (
CancelledError,
Deferred,
gatherResults,
passthru,
succeed,
)
from twisted.python.failure import Failure
from twisted.test.testutils import XMLAssertionMixin
from twisted.trial.unittest import SynchronousTestCase
from twisted.web._flatten import BUFFER_SIZE
from twisted.web.error import FlattenerError, UnfilledSlot, UnsupportedType
from twisted.web.iweb import IRenderable, IRequest, ITemplateLoader
from twisted.web.template import (
CDATA,
CharRef,
Comment,
Element,
Flattenable,
Tag,
TagLoader,
flatten,
flattenString,
renderer,
slot,
tags,
)
from twisted.web.test._util import FlattenTestCase
IS_PYTHON_313 = sys.version_info[:2] >= (3, 13)
class SerializationTests(FlattenTestCase, XMLAssertionMixin):
"""
Tests for flattening various things.
"""
def test_nestedTags(self) -> None:
"""
Test that nested tags flatten correctly.
"""
self.assertFlattensImmediately(
tags.html(tags.body("42"), hi="there"),
b'<html hi="there"><body>42</body></html>',
)
def test_serializeString(self) -> None:
"""
Test that strings will be flattened and escaped correctly.
"""
self.assertFlattensImmediately("one", b"one"),
self.assertFlattensImmediately("<abc&&>123", b"&lt;abc&amp;&amp;&gt;123"),
def test_serializeSelfClosingTags(self) -> None:
"""
The serialized form of a self-closing tag is C{'<tagName />'}.
"""
self.assertFlattensImmediately(tags.img(), b"<img />")
def test_serializeAttribute(self) -> None:
"""
The serialized form of attribute I{a} with value I{b} is C{'a="b"'}.
"""
self.assertFlattensImmediately(tags.img(src="foo"), b'<img src="foo" />')
def test_serializedMultipleAttributes(self) -> None:
"""
Multiple attributes are separated by a single space in their serialized
form.
"""
tag = tags.img()
tag.attributes = OrderedDict([("src", "foo"), ("name", "bar")])
self.assertFlattensImmediately(tag, b'<img src="foo" name="bar" />')
def checkAttributeSanitization(
self,
wrapData: Callable[[str], Flattenable],
wrapTag: Callable[[Tag], Flattenable],
) -> None:
"""
Common implementation of L{test_serializedAttributeWithSanitization}
and L{test_serializedDeferredAttributeWithSanitization},
L{test_serializedAttributeWithTransparentTag}.
@param wrapData: A 1-argument callable that wraps around the
attribute's value so other tests can customize it.
@param wrapTag: A 1-argument callable that wraps around the outer tag
so other tests can customize it.
"""
self.assertFlattensImmediately(
wrapTag(tags.img(src=wrapData('<>&"'))),
b'<img src="&lt;&gt;&amp;&quot;" />',
)
def test_serializedAttributeWithSanitization(self) -> None:
"""
Attribute values containing C{"<"}, C{">"}, C{"&"}, or C{'"'} have
C{"&lt;"}, C{"&gt;"}, C{"&amp;"}, or C{"&quot;"} substituted for those
bytes in the serialized output.
"""
self.checkAttributeSanitization(passthru, passthru)
def test_serializedDeferredAttributeWithSanitization(self) -> None:
"""
Like L{test_serializedAttributeWithSanitization}, but when the contents
of the attribute are in a L{Deferred
<twisted.internet.defer.Deferred>}.
"""
self.checkAttributeSanitization(succeed, passthru)
def test_serializedAttributeWithSlotWithSanitization(self) -> None:
"""
Like L{test_serializedAttributeWithSanitization} but with a slot.
"""
toss = []
def insertSlot(value: str) -> Flattenable:
toss.append(value)
return slot("stuff")
def fillSlot(tag: Tag) -> Tag:
return tag.fillSlots(stuff=toss.pop())
self.checkAttributeSanitization(insertSlot, fillSlot)
def test_serializedAttributeWithTransparentTag(self) -> None:
"""
Attribute values which are supplied via the value of a C{t:transparent}
tag have the same substitution rules to them as values supplied
directly.
"""
self.checkAttributeSanitization(tags.transparent, passthru)
def test_serializedAttributeWithTransparentTagWithRenderer(self) -> None:
"""
Like L{test_serializedAttributeWithTransparentTag}, but when the
attribute is rendered by a renderer on an element.
"""
class WithRenderer(Element):
def __init__(self, value: str, loader: Optional[ITemplateLoader]) -> None:
self.value = value
super().__init__(loader)
@renderer
def stuff(self, request: Optional[IRequest], tag: Tag) -> Flattenable:
return self.value
toss = []
def insertRenderer(value: str) -> Flattenable:
toss.append(value)
return tags.transparent(render="stuff")
def render(tag: Tag) -> Flattenable:
return WithRenderer(toss.pop(), TagLoader(tag))
self.checkAttributeSanitization(insertRenderer, render)
def test_serializedAttributeWithRenderable(self) -> None:
"""
Like L{test_serializedAttributeWithTransparentTag}, but when the
attribute is a provider of L{IRenderable} rather than a transparent
tag.
"""
@implementer(IRenderable)
class Arbitrary:
def __init__(self, value: Flattenable) -> None:
self.value = value
def render(self, request: Optional[IRequest]) -> Flattenable:
return self.value
def lookupRenderMethod(
self, name: str
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
raise NotImplementedError("Unexpected call")
self.checkAttributeSanitization(Arbitrary, passthru)
def checkTagAttributeSerialization(
self, wrapTag: Callable[[Tag], Flattenable]
) -> None:
"""
Common implementation of L{test_serializedAttributeWithTag} and
L{test_serializedAttributeWithDeferredTag}.
@param wrapTag: A 1-argument callable that wraps around the attribute's
value so other tests can customize it.
@type wrapTag: callable taking L{Tag} and returning something
flattenable
"""
innerTag = tags.a('<>&"')
outerTag = tags.img(src=wrapTag(innerTag))
outer = self.assertFlattensImmediately(
outerTag,
b'<img src="&lt;a&gt;&amp;lt;&amp;gt;&amp;amp;&quot;&lt;/a&gt;" />',
)
inner = self.assertFlattensImmediately(innerTag, b'<a>&lt;&gt;&amp;"</a>')
# Since the above quoting is somewhat tricky, validate it by making sure
# that the main use-case for tag-within-attribute is supported here: if
# we serialize a tag, it is quoted *such that it can be parsed out again
# as a tag*.
self.assertXMLEqual(XML(outer).attrib["src"], inner)
def test_serializedAttributeWithTag(self) -> None:
"""
L{Tag} objects which are serialized within the context of an attribute
are serialized such that the text content of the attribute may be
parsed to retrieve the tag.
"""
self.checkTagAttributeSerialization(passthru)
def test_serializedAttributeWithDeferredTag(self) -> None:
"""
Like L{test_serializedAttributeWithTag}, but when the L{Tag} is in a
L{Deferred <twisted.internet.defer.Deferred>}.
"""
self.checkTagAttributeSerialization(succeed)
def test_serializedAttributeWithTagWithAttribute(self) -> None:
"""
Similar to L{test_serializedAttributeWithTag}, but for the additional
complexity where the tag which is the attribute value itself has an
attribute value which contains bytes which require substitution.
"""
flattened = self.assertFlattensImmediately(
tags.img(src=tags.a(href='<>&"')),
b'<img src="&lt;a href='
b"&quot;&amp;lt;&amp;gt;&amp;amp;&amp;quot;&quot;&gt;"
b'&lt;/a&gt;" />',
)
# As in checkTagAttributeSerialization, belt-and-suspenders:
self.assertXMLEqual(
XML(flattened).attrib["src"], b'<a href="&lt;&gt;&amp;&quot;"></a>'
)
def test_serializeComment(self) -> None:
"""
Test that comments are correctly flattened and escaped.
"""
self.assertFlattensImmediately(Comment("foo bar"), b"<!--foo bar-->")
def test_commentEscaping(self) -> Deferred[List[bytes]]:
"""
The data in a L{Comment} is escaped and mangled in the flattened output
so that the result can be safely included in an HTML document.
Test that C{>} is escaped when the sequence C{-->} is encountered
within a comment, and that comments do not end with C{-}.
"""
def verifyComment(c: bytes) -> None:
self.assertTrue(
c.startswith(b"<!--"),
f"{c!r} does not start with the comment prefix",
)
self.assertTrue(
c.endswith(b"-->"),
f"{c!r} does not end with the comment suffix",
)
# If it is shorter than 7, then the prefix and suffix overlap
# illegally.
self.assertTrue(len(c) >= 7, f"{c!r} is too short to be a legal comment")
content = c[4:-3]
if b"foo" in content:
self.assertIn(b">", content)
else:
self.assertNotIn(b">", content)
if content:
self.assertNotEqual(content[-1], b"-")
results = []
for c in [
"",
"foo > bar",
"abracadabra-",
"not-->magic",
]:
d = flattenString(None, Comment(c))
d.addCallback(verifyComment)
results.append(d)
return gatherResults(results)
def test_serializeCDATA(self) -> None:
"""
Test that CDATA is correctly flattened and escaped.
"""
self.assertFlattensImmediately(CDATA("foo bar"), b"<![CDATA[foo bar]]>"),
self.assertFlattensImmediately(
CDATA("foo ]]> bar"), b"<![CDATA[foo ]]]]><![CDATA[> bar]]>"
)
def test_serializeUnicode(self) -> None:
"""
Test that unicode is encoded correctly in the appropriate places, and
raises an error when it occurs in inappropriate place.
"""
snowman = "\N{SNOWMAN}"
self.assertFlattensImmediately(snowman, b"\xe2\x98\x83")
self.assertFlattensImmediately(tags.p(snowman), b"<p>\xe2\x98\x83</p>")
self.assertFlattensImmediately(Comment(snowman), b"<!--\xe2\x98\x83-->")
self.assertFlattensImmediately(CDATA(snowman), b"<![CDATA[\xe2\x98\x83]]>")
self.assertFlatteningRaises(Tag(snowman), UnicodeEncodeError)
self.assertFlatteningRaises(
Tag("p", attributes={snowman: ""}), UnicodeEncodeError
)
def test_serializeCharRef(self) -> None:
"""
A character reference is flattened to a string using the I{&#NNNN;}
syntax.
"""
ref = CharRef(ord("\N{SNOWMAN}"))
self.assertFlattensImmediately(ref, b"&#9731;")
def test_serializeDeferred(self) -> None:
"""
Test that a deferred is substituted with the current value in the
callback chain when flattened.
"""
self.assertFlattensImmediately(succeed("two"), b"two")
def test_serializeSameDeferredTwice(self) -> None:
"""
Test that the same deferred can be flattened twice.
"""
d = succeed("three")
self.assertFlattensImmediately(d, b"three")
self.assertFlattensImmediately(d, b"three")
def test_serializeCoroutine(self) -> None:
"""
Test that a coroutine returning a value is substituted with the that
value when flattened.
"""
from textwrap import dedent
namespace: Dict[str, FunctionType] = {}
exec(
dedent(
"""
async def coro(x):
return x
"""
),
namespace,
)
coro = namespace["coro"]
self.assertFlattensImmediately(coro("four"), b"four")
def test_serializeCoroutineWithAwait(self) -> None:
"""
Test that a coroutine returning an awaited deferred value is
substituted with that value when flattened.
"""
from textwrap import dedent
namespace = dict(succeed=succeed)
exec(
dedent(
"""
async def coro(x):
return await succeed(x)
"""
),
namespace,
)
coro = namespace["coro"]
self.assertFlattensImmediately(coro("four"), b"four")
def test_serializeIRenderable(self) -> None:
"""
Test that flattening respects all of the IRenderable interface.
"""
@implementer(IRenderable)
class FakeElement:
def render(ign, ored: object) -> Tag:
return tags.p(
"hello, ",
tags.transparent(render="test"),
" - ",
tags.transparent(render="test"),
)
def lookupRenderMethod(
ign, name: str
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
self.assertEqual(name, "test")
return lambda ign, node: node("world")
self.assertFlattensImmediately(FakeElement(), b"<p>hello, world - world</p>")
def test_serializeMissingRenderFactory(self) -> None:
"""
Test that flattening a tag with a C{render} attribute when no render
factory is available in the context raises an exception.
"""
self.assertFlatteningRaises(tags.transparent(render="test"), ValueError)
def test_serializeSlots(self) -> None:
"""
Test that flattening a slot will use the slot value from the tag.
"""
t1 = tags.p(slot("test"))
t2 = t1.clone()
t2.fillSlots(test="hello, world")
self.assertFlatteningRaises(t1, UnfilledSlot)
self.assertFlattensImmediately(t2, b"<p>hello, world</p>")
def test_serializeDeferredSlots(self) -> None:
"""
Test that a slot with a deferred as its value will be flattened using
the value from the deferred.
"""
t = tags.p(slot("test"))
t.fillSlots(test=succeed(tags.em("four>")))
self.assertFlattensImmediately(t, b"<p><em>four&gt;</em></p>")
def test_unknownTypeRaises(self) -> None:
"""
Test that flattening an unknown type of thing raises an exception.
"""
self.assertFlatteningRaises(None, UnsupportedType) # type: ignore[arg-type]
class FlattenChunkingTests(SynchronousTestCase):
"""
Tests for the way pieces of the result are chunked together in calls to
the write function.
"""
def test_oneSmallChunk(self) -> None:
"""
If the entire value to be flattened is available synchronously and fits
into the buffer it is all passed to a single call to the write
function.
"""
output: List[bytes] = []
self.successResultOf(flatten(None, ["1", "2", "3"], output.append))
assert_that(output, equal_to([b"123"]))
def test_someLargeChunks(self) -> None:
"""
If the entire value to be flattened is available synchronously but does
not fit into the buffer then it is chunked into buffer-sized pieces
and these are passed to the write function.
"""
some = ["x"] * BUFFER_SIZE
someMore = ["y"] * BUFFER_SIZE
evenMore = ["z"] * BUFFER_SIZE
output: List[bytes] = []
self.successResultOf(flatten(None, [some, someMore, evenMore], output.append))
assert_that(
output,
equal_to([b"x" * BUFFER_SIZE, b"y" * BUFFER_SIZE, b"z" * BUFFER_SIZE]),
)
def _chunksSeparatedByAsyncTest(
self,
start: Callable[
[Flattenable], Tuple[Deferred[Flattenable], Callable[[], object]]
],
) -> None:
"""
Assert that flattening with a L{Deferred} returned by C{start} results
in the expected buffering behavior.
The L{Deferred} need not have a result by it is returned by C{start}
but must have a result after the callable returned along with it is
called.
The expected buffering behavior is that flattened values up to the
L{Deferred} are written together and then the result of the
L{Deferred} is written together with values following it up to the
next L{Deferred}.
"""
first_wait, first_finish = start("first-")
second_wait, second_finish = start("second-")
value = [
"already-available",
"-chunks",
first_wait,
"chunks-already-",
"computed",
second_wait,
"more-chunks-",
"already-available",
]
output: List[bytes] = []
d = flatten(None, value, output.append)
first_finish()
second_finish()
self.successResultOf(d)
assert_that(
output,
equal_to(
[
b"already-available-chunks",
b"first-chunks-already-computed",
b"second-more-chunks-already-available",
]
),
)
def test_chunksSeparatedByFiredDeferred(self) -> None:
"""
When a fired L{Deferred} is encountered any buffered data is
passed to the write function. Then the L{Deferred}'s result is passed
to another write along with following synchronous values.
This exact buffering behavior should be considered an implementation
detail and can be replaced by some other better behavior in the future
if someone wants.
"""
def sync_start(
v: Flattenable,
) -> Tuple[Deferred[Flattenable], Callable[[], None]]:
return (succeed(v), lambda: None)
self._chunksSeparatedByAsyncTest(sync_start)
def test_chunksSeparatedByUnfiredDeferred(self) -> None:
"""
When an unfired L{Deferred} is encountered any buffered data is
passed to the write function. After the result of the L{Deferred} is
available it is passed to another write along with following
synchronous values.
"""
def async_start(
v: Flattenable,
) -> Tuple[Deferred[Flattenable], Callable[[], None]]:
d: Deferred[Flattenable] = Deferred()
return (d, lambda: d.callback(v))
self._chunksSeparatedByAsyncTest(async_start)
# Use the co_filename mechanism (instead of the __file__ mechanism) because
# it is the mechanism traceback formatting uses. The two do not necessarily
# agree with each other. This requires a code object compiled in this file.
# The easiest way to get a code object is with a new function. I'll use a
# lambda to avoid adding anything else to this namespace. The result will
# be a string which agrees with the one the traceback module will put into a
# traceback for frames associated with functions defined in this file.
HERE = (lambda: None).__code__.co_filename
class FlattenerErrorTests(SynchronousTestCase):
"""
Tests for L{FlattenerError}.
"""
def test_renderable(self) -> None:
"""
If a L{FlattenerError} is created with an L{IRenderable} provider root,
the repr of that object is included in the string representation of the
exception.
"""
@implementer(IRenderable)
class Renderable: # type: ignore[misc]
def __repr__(self) -> str:
return "renderable repr"
self.assertEqual(
str(FlattenerError(RuntimeError("reason"), [Renderable()], [])),
"Exception while flattening:\n"
" renderable repr\n"
"RuntimeError: reason\n",
)
def test_tag(self) -> None:
"""
If a L{FlattenerError} is created with a L{Tag} instance with source
location information, the source location is included in the string
representation of the exception.
"""
tag = Tag("div", filename="/foo/filename.xhtml", lineNumber=17, columnNumber=12)
self.assertEqual(
str(FlattenerError(RuntimeError("reason"), [tag], [])),
"Exception while flattening:\n"
' File "/foo/filename.xhtml", line 17, column 12, in "div"\n'
"RuntimeError: reason\n",
)
def test_tagWithoutLocation(self) -> None:
"""
If a L{FlattenerError} is created with a L{Tag} instance without source
location information, only the tagName is included in the string
representation of the exception.
"""
self.assertEqual(
str(FlattenerError(RuntimeError("reason"), [Tag("span")], [])),
"Exception while flattening:\n" " Tag <span>\n" "RuntimeError: reason\n",
)
def test_traceback(self) -> None:
"""
If a L{FlattenerError} is created with traceback frames, they are
included in the string representation of the exception.
"""
# Try to be realistic in creating the data passed in for the traceback
# frames.
def f() -> None:
g()
def g() -> NoReturn:
raise RuntimeError("reason")
try:
f()
except RuntimeError as e:
# Get the traceback, minus the info for *this* frame
tbinfo = traceback.extract_tb(sys.exc_info()[2])[1:]
exc = e
else:
self.fail("f() must raise RuntimeError")
if IS_PYTHON_313:
column_marker = " ~^^\n"
else:
column_marker = ""
self.assertEqual(
str(FlattenerError(exc, [], tbinfo)),
"Exception while flattening:\n"
' File "%s", line %d, in f\n'
" g()\n"
"%s"
' File "%s", line %d, in g\n'
' raise RuntimeError("reason")\n'
"RuntimeError: reason\n"
% (
HERE,
f.__code__.co_firstlineno + 1,
column_marker,
HERE,
g.__code__.co_firstlineno + 1,
),
)
def test_asynchronousFlattenError(self) -> None:
"""
When flattening a renderer which raises an exception asynchronously,
the error is reported when it occurs.
"""
failing: Deferred[object] = Deferred()
@implementer(IRenderable)
class NotActuallyRenderable:
"No methods provided; this will fail"
def __repr__(self) -> str:
return "<unrenderable>"
def lookupRenderMethod( # type: ignore[empty-body]
self, name: str
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
...
def render(self, request: Optional[IRequest]) -> Flattenable:
return failing
flattening = flattenString(None, [NotActuallyRenderable()])
self.assertNoResult(flattening)
exc = RuntimeError("example")
failing.errback(exc)
failure = self.failureResultOf(flattening, FlattenerError)
if IS_PYTHON_313:
column_marker = ".*\n.*\n.*\nRuntimeError: example\n"
else:
column_marker = ""
self.assertRegex(
str(failure.value),
re.compile(
dedent(
"""\
Exception while flattening:
\\[<unrenderable>\\]
<unrenderable>
<Deferred at .* current result: <twisted.python.failure.Failure builtins.RuntimeError: example>>
File ".*", line \\d*, in _flattenTree
element = await element.*
"""
)
+ column_marker,
flags=re.MULTILINE,
),
)
self.assertIn("RuntimeError: example", str(failure.value))
# The original exception is unmodified and will be logged separately if
# unhandled.
self.failureResultOf(failing, RuntimeError)
def test_cancel(self) -> None:
"""
The flattening of a Deferred can be cancelled.
"""
cancelCount = 0
cancelArg = None
def checkCancel(cancelled: Deferred[object]) -> None:
nonlocal cancelArg, cancelCount
cancelArg = cancelled
cancelCount += 1
err = None
def saveErr(failure: Failure) -> None:
nonlocal err
err = failure
d: Deferred[object] = Deferred(checkCancel)
flattening = flattenString(None, d)
self.assertNoResult(flattening)
d.addErrback(saveErr)
flattening.cancel()
# Check whether we got an orderly cancellation.
# Do this first to get more meaningful reporting if something crashed.
failure = self.failureResultOf(flattening, FlattenerError)
self.assertEqual(cancelCount, 1)
self.assertIs(cancelArg, d)
self.assertIsInstance(err, Failure)
self.assertIsInstance(cast(Failure, err).value, CancelledError)
exc = failure.value.args[0]
self.assertIsInstance(exc, CancelledError)

View File

@@ -0,0 +1,41 @@
# 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) -> None:
"""
Calls to L{twisted.web.html} members emit a deprecation warning.
"""
def assertDeprecationWarningOf(method: str) -> None:
"""
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")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,694 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.http_headers}.
"""
from __future__ import annotations
from typing import Sequence
from twisted.trial.unittest import SynchronousTestCase, TestCase
from twisted.web.http_headers import Headers, InvalidHeaderName, _nameEncoder
from twisted.web.test.requesthelper import (
bytesLinearWhitespaceComponents,
sanitizedBytes,
textLinearWhitespaceComponents,
)
class NameEncoderTests(SynchronousTestCase):
"""
Test L{twisted.web.http_headers._NameEncoder}
"""
def test_encodeName(self) -> None:
"""
L{_NameEncoder.encode} returns the canonical capitalization for
the given header.
"""
self.assertEqual(_nameEncoder.encode(b"test"), b"Test")
self.assertEqual(_nameEncoder.encode(b"test-stuff"), b"Test-Stuff")
self.assertEqual(_nameEncoder.encode(b"content-md5"), b"Content-MD5")
self.assertEqual(_nameEncoder.encode(b"dnt"), b"DNT")
self.assertEqual(_nameEncoder.encode(b"etag"), b"ETag")
self.assertEqual(_nameEncoder.encode(b"p3p"), b"P3P")
self.assertEqual(_nameEncoder.encode(b"te"), b"TE")
self.assertEqual(_nameEncoder.encode(b"www-authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode(b"WWW-authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode(b"Www-Authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode(b"x-xss-protection"), b"X-XSS-Protection")
def test_encodeNameStr(self) -> None:
"""
L{_NameEncoder.encode} returns the canonical capitalization for
a header name given as a L{str}.
"""
self.assertEqual(_nameEncoder.encode("test"), b"Test")
self.assertEqual(_nameEncoder.encode("test-stuff"), b"Test-Stuff")
self.assertEqual(_nameEncoder.encode("content-md5"), b"Content-MD5")
self.assertEqual(_nameEncoder.encode("dnt"), b"DNT")
self.assertEqual(_nameEncoder.encode("etag"), b"ETag")
self.assertEqual(_nameEncoder.encode("p3p"), b"P3P")
self.assertEqual(_nameEncoder.encode("te"), b"TE")
self.assertEqual(_nameEncoder.encode("www-authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode("WWW-authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode("Www-Authenticate"), b"WWW-Authenticate")
self.assertEqual(_nameEncoder.encode("x-xss-protection"), b"X-XSS-Protection")
def test_maxCachedHeaders(self) -> None:
"""
Only a limited number of HTTP header names get cached.
"""
headers = Headers()
for i in range(_nameEncoder._MAX_CACHED_HEADERS + 200):
headers.addRawHeader(f"hello-{i}", "value")
self.assertEqual(
len(_nameEncoder._canonicalHeaderCache), _nameEncoder._MAX_CACHED_HEADERS
)
def assertSanitized(
testCase: TestCase, components: Sequence[bytes] | Sequence[str], expected: bytes
) -> None:
"""
Assert that the components are sanitized to the expected value as
both a header 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 values; see
C{textLinearWhitespaceComponents} and
C{bytesLinearWhitespaceComponents}
@param expected: The expected sanitized form of the component as
a header value.
"""
name = b"Name"
for component in components:
headers = []
headers.append(Headers({name: [component]})) # type: ignore[misc]
added = Headers()
added.addRawHeader(name, component)
headers.append(added)
setHeader = Headers()
setHeader.setRawHeaders(name, [component])
headers.append(setHeader)
for header in headers:
testCase.assertEqual(list(header.getAllRawHeaders()), [(name, [expected])])
testCase.assertEqual(header.getRawHeaders(name), [expected])
class BytesHeadersTests(TestCase):
"""
Tests for L{Headers}, using L{bytes} arguments for methods.
"""
def test_sanitizeLinearWhitespace(self) -> None:
"""
Linear whitespace in header names or values is replaced with a
single space.
"""
assertSanitized(self, bytesLinearWhitespaceComponents, sanitizedBytes)
def test_initializer(self) -> None:
"""
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) -> None:
"""
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_addRawHeader(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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 = ["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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{Headers.hasHeader} returns C{False} when the given header is not
found.
"""
self.assertFalse(Headers().hasHeader(b"test"))
def test_removeHeader(self) -> None:
"""
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) -> None:
"""
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_getAllRawHeaders(self) -> None:
"""
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 = {(k, tuple(v)) for k, v in h.getAllRawHeaders()}
self.assertEqual(
allHeaders,
{(b"WWW-Authenticate", (b"basic aksljdlk=",)), (b"Test", (b"lemurs",))},
)
def test_headersComparison(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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]})),
f"Headers({{{foo.capitalize()!r}: [{bar!r}, {baz!r}]}})",
)
def test_reprWithRawBytes(self) -> None:
"""
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]})),
f"Headers({{{foo.capitalize()!r}: [{bar!r}, {baz!r}]}})",
)
def test_subclassRepr(self) -> None:
"""
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]})),
f"FunnyHeaders({{{foo.capitalize()!r}: [{bar!r}, {baz!r}]}})",
)
def test_copy(self) -> None:
"""
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{str} arguments for methods.
"""
def test_sanitizeLinearWhitespace(self) -> None:
"""
Linear whitespace in header names or values is replaced with a
single space.
"""
assertSanitized(self, textLinearWhitespaceComponents, sanitizedBytes)
def test_initializer(self) -> None:
"""
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{str} argument is given, it returns
L{str} values. Both are the same header value, just encoded or
decoded.
"""
h = Headers({"Foo": ["bar"]})
self.assertEqual(h.getRawHeaders(b"foo"), [b"bar"])
self.assertEqual(h.getRawHeaders("foo"), ["bar"])
def test_setRawHeaders(self) -> None:
"""
L{Headers.setRawHeaders} sets the header values for the given
header name to the sequence of strings, encoded.
"""
rawValue = ["value1", "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) -> None:
"""
Passing L{str} 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("\u2603", ["val"])
with self.assertRaises(UnicodeEncodeError):
h.hasHeader("\u2603")
def test_nameNotToken(self) -> None:
"""
HTTP header names must be tokens, so any names containing non-token
characters raises L{InvalidHeaderName}
"""
h = Headers()
# A non-token character within ISO-8851-1
self.assertRaises(InvalidHeaderName, h.setRawHeaders, b"\xe1", [b"val"])
self.assertRaises(InvalidHeaderName, h.setRawHeaders, "\u00e1", [b"val"])
# Whitespace
self.assertRaises(InvalidHeaderName, h.setRawHeaders, b"a b", [b"val"])
self.assertRaises(InvalidHeaderName, h.setRawHeaders, "c\nd", [b"val"])
self.assertRaises(InvalidHeaderName, h.setRawHeaders, "c\td", [b"val"])
def test_nameEncoding(self) -> None:
"""
Passing L{str} 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("bar", [b"foo"])
# It's encoded to the ISO-8859-1 value, which we can use to access it
self.assertTrue(h.hasHeader(b"bar"))
self.assertEqual(h.getRawHeaders(b"bar"), [b"foo"])
# We can still access it using the Unicode string..
self.assertTrue(h.hasHeader("bar"))
def test_rawHeadersValueEncoding(self) -> None:
"""
Passing L{str} to L{Headers.setRawHeaders} will encode the name as
ISO-8859-1 and values as UTF-8.
"""
h = Headers()
h.setRawHeaders("x", ["\u2603", b"foo"])
self.assertTrue(h.hasHeader(b"x"))
self.assertEqual(h.getRawHeaders(b"x"), [b"\xe2\x98\x83", b"foo"])
def test_addRawHeader(self) -> None:
"""
L{Headers.addRawHeader} adds a new value for a given header.
"""
h = Headers()
h.addRawHeader("test", "lemur")
self.assertEqual(h.getRawHeaders("test"), ["lemur"])
h.addRawHeader("test", "panda")
self.assertEqual(h.getRawHeaders("test"), ["lemur", "panda"])
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur", b"panda"])
def test_getRawHeadersNoDefault(self) -> None:
"""
L{Headers.getRawHeaders} returns L{None} if the header is not found and
no default is specified.
"""
self.assertIsNone(Headers().getRawHeaders("test"))
def test_getRawHeadersDefaultValue(self) -> None:
"""
L{Headers.getRawHeaders} returns the specified default value when no
header is found.
"""
h = Headers()
default = object()
self.assertIdentical(h.getRawHeaders("test", default), default)
self.assertIdentical(h.getRawHeaders("test", None), None)
self.assertEqual(h.getRawHeaders("test", [None]), [None])
self.assertEqual(
h.getRawHeaders("test", ["\N{SNOWMAN}"]),
["\N{SNOWMAN}"],
)
def test_getRawHeadersWithDefaultMatchingValue(self) -> None:
"""
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("key", default)[0], str)
self.assertEqual(h.getRawHeaders("key", default), ["value"])
def test_getRawHeaders(self) -> None:
"""
L{Headers.getRawHeaders} returns the values which have been set for a
given header.
"""
h = Headers()
h.setRawHeaders("test", ["lemur"])
self.assertEqual(h.getRawHeaders("test"), ["lemur"])
self.assertEqual(h.getRawHeaders("Test"), ["lemur"])
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur"])
self.assertEqual(h.getRawHeaders(b"Test"), [b"lemur"])
def test_hasHeaderTrue(self) -> None:
"""
Check that L{Headers.hasHeader} returns C{True} when the given header
is found.
"""
h = Headers()
h.setRawHeaders("test", ["lemur"])
self.assertTrue(h.hasHeader("test"))
self.assertTrue(h.hasHeader("Test"))
self.assertTrue(h.hasHeader(b"test"))
self.assertTrue(h.hasHeader(b"Test"))
def test_hasHeaderFalse(self) -> None:
"""
L{Headers.hasHeader} returns C{False} when the given header is not
found.
"""
self.assertFalse(Headers().hasHeader("test"))
def test_removeHeader(self) -> None:
"""
Check that L{Headers.removeHeader} removes the given header.
"""
h = Headers()
h.setRawHeaders("foo", ["lemur"])
self.assertTrue(h.hasHeader("foo"))
h.removeHeader("foo")
self.assertFalse(h.hasHeader("foo"))
self.assertFalse(h.hasHeader(b"foo"))
h.setRawHeaders("bar", ["panda"])
self.assertTrue(h.hasHeader("bar"))
h.removeHeader("Bar")
self.assertFalse(h.hasHeader("bar"))
self.assertFalse(h.hasHeader(b"bar"))
def test_removeHeaderDoesntExist(self) -> None:
"""
L{Headers.removeHeader} is a no-operation when the specified header is
not found.
"""
h = Headers()
h.removeHeader("test")
self.assertEqual(list(h.getAllRawHeaders()), [])
def test_getAllRawHeaders(self) -> None:
"""
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("test", ["lemurs"])
h.setRawHeaders("www-authenticate", ["basic aksljdlk="])
h.setRawHeaders("content-md5", ["kjdfdfgdfgnsd"])
allHeaders = {(k, tuple(v)) for k, v in h.getAllRawHeaders()}
self.assertEqual(
allHeaders,
{
(b"WWW-Authenticate", (b"basic aksljdlk=",)),
(b"Content-MD5", (b"kjdfdfgdfgnsd",)),
(b"Test", (b"lemurs",)),
},
)
def test_headersComparison(self) -> None:
"""
A L{Headers} instance compares equal to itself and to another
L{Headers} instance with the same values.
"""
first = Headers()
first.setRawHeaders("foo", ["panda"])
second = Headers()
second.setRawHeaders("foo", ["panda"])
third = Headers()
third.setRawHeaders("foo", ["lemur", "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", [b"panda"])
secondBytes = Headers()
secondBytes.setRawHeaders(b"foo", [b"panda"])
thirdBytes = Headers()
thirdBytes.setRawHeaders(b"foo", [b"lemur", "panda"])
self.assertEqual(first, firstBytes)
self.assertEqual(second, secondBytes)
self.assertEqual(third, thirdBytes)
def test_otherComparison(self) -> None:
"""
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, "foo")
def test_repr(self) -> None:
"""
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 = "foo"
bar = "bar\u2603"
baz = "baz"
fooEncoded = "'Foo'"
barEncoded = "'bar\\xe2\\x98\\x83'"
fooEncoded = "b" + fooEncoded
barEncoded = "b" + barEncoded
self.assertEqual(
repr(Headers({foo: [bar, baz]})),
"Headers({{{}: [{}, {!r}]}})".format(
fooEncoded, barEncoded, baz.encode("utf8")
),
)
def test_subclassRepr(self) -> None:
"""
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 = "foo"
bar = "bar\u2603"
baz = "baz"
fooEncoded = "b'Foo'"
barEncoded = "b'bar\\xe2\\x98\\x83'"
class FunnyHeaders(Headers):
pass
self.assertEqual(
repr(FunnyHeaders({foo: [bar, baz]})),
"FunnyHeaders({%s: [%s, %r]})"
% (fooEncoded, barEncoded, baz.encode("utf8")),
)
def test_copy(self) -> None:
"""
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("test", ["foo\u2603"])
i = h.copy()
# The copy contains the same value as the original
self.assertEqual(i.getRawHeaders("test"), ["foo\u2603"])
self.assertEqual(i.getRawHeaders(b"test"), [b"foo\xe2\x98\x83"])
# Add a header to the original
h.addRawHeader("test", "bar")
# Verify that the copy has not changed
self.assertEqual(i.getRawHeaders("test"), ["foo\u2603"])
self.assertEqual(i.getRawHeaders(b"test"), [b"foo\xe2\x98\x83"])
# Add a header to the copy
i.addRawHeader("Test", b"baz")
# Verify that the orignal does not have it
self.assertEqual(h.getRawHeaders("test"), ["foo\u2603", "bar"])
self.assertEqual(h.getRawHeaders(b"test"), [b"foo\xe2\x98\x83", b"bar"])
class MixedHeadersTests(TestCase):
"""
Tests for L{Headers}, mixing L{bytes} and L{str} arguments for methods
where that is permitted.
"""
def test_addRawHeader(self) -> None:
"""
L{Headers.addRawHeader} accepts mixed L{str} and L{bytes}.
"""
h = Headers()
h.addRawHeader(b"bytes", "str")
h.addRawHeader("str", b"bytes")
self.assertEqual(h.getRawHeaders(b"Bytes"), [b"str"])
self.assertEqual(h.getRawHeaders("Str"), ["bytes"])
def test_setRawHeaders(self) -> None:
"""
L{Headers.setRawHeaders} accepts mixed L{str} and L{bytes}.
"""
h = Headers()
h.setRawHeaders(b"bytes", [b"bytes"])
h.setRawHeaders("str", ["str"])
h.setRawHeaders("mixed-str", [b"bytes", "str"])
h.setRawHeaders(b"mixed-bytes", ["str", b"bytes"])
self.assertEqual(h.getRawHeaders(b"Bytes"), [b"bytes"])
self.assertEqual(h.getRawHeaders("Str"), ["str"])
self.assertEqual(h.getRawHeaders("Mixed-Str"), ["bytes", "str"])
self.assertEqual(h.getRawHeaders(b"Mixed-Bytes"), [b"str", b"bytes"])

View File

@@ -0,0 +1,644 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web._auth}.
"""
import base64
from zope.interface import implementer
from zope.interface.verify import verifyObject
from twisted.cred import error, portal
from twisted.cred.checkers import (
ANONYMOUS,
AllowAnonymousAccess,
InMemoryUsernamePasswordDatabaseDontUse,
)
from twisted.cred.credentials import IUsernamePassword
from twisted.internet.address import IPv4Address
from twisted.internet.error import ConnectionDone
from twisted.internet.testing import EventLoggingObserver
from twisted.logger import globalLogPublisher
from twisted.python.failure import Failure
from twisted.trial import unittest
from twisted.web._auth import basic, digest
from twisted.web._auth.basic import BasicCredentialFactory
from twisted.web._auth.wrapper import HTTPAuthSessionWrapper, UnauthorizedResource
from twisted.web.iweb import ICredentialFactory
from twisted.web.resource import IResource, Resource, getChildForRequest
from twisted.web.server import NOT_DONE_YET
from twisted.web.static import Data
from twisted.web.test.test_web import DummyRequest
def b64encode(s):
return base64.b64encode(s).strip()
class BasicAuthTestsMixin:
"""
L{TestCase} mixin class which defines a number of tests for
L{basic.BasicCredentialFactory}. Because this mixin defines C{setUp}, it
must be inherited before L{TestCase}.
"""
def setUp(self):
self.request = self.makeRequest()
self.realm = b"foo"
self.username = b"dreid"
self.password = b"S3CuR1Ty"
self.credentialFactory = basic.BasicCredentialFactory(self.realm)
def makeRequest(self, method=b"GET", clientAddress=None):
"""
Create a request object to be passed to
L{basic.BasicCredentialFactory.decode} along with a response value.
Override this in a subclass.
"""
raise NotImplementedError(f"{self.__class__!r} did not implement makeRequest")
def test_interface(self):
"""
L{BasicCredentialFactory} implements L{ICredentialFactory}.
"""
self.assertTrue(verifyObject(ICredentialFactory, self.credentialFactory))
def test_usernamePassword(self):
"""
L{basic.BasicCredentialFactory.decode} turns a base64-encoded response
into a L{UsernamePassword} object with a password which reflects the
one which was encoded in the response.
"""
response = b64encode(b"".join([self.username, b":", self.password]))
creds = self.credentialFactory.decode(response, self.request)
self.assertTrue(IUsernamePassword.providedBy(creds))
self.assertTrue(creds.checkPassword(self.password))
self.assertFalse(creds.checkPassword(self.password + b"wrong"))
def test_incorrectPadding(self):
"""
L{basic.BasicCredentialFactory.decode} decodes a base64-encoded
response with incorrect padding.
"""
response = b64encode(b"".join([self.username, b":", self.password]))
response = response.strip(b"=")
creds = self.credentialFactory.decode(response, self.request)
self.assertTrue(verifyObject(IUsernamePassword, creds))
self.assertTrue(creds.checkPassword(self.password))
def test_invalidEncoding(self):
"""
L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} if passed
a response which is not base64-encoded.
"""
response = b"x" # one byte cannot be valid base64 text
self.assertRaises(
error.LoginFailed,
self.credentialFactory.decode,
response,
self.makeRequest(),
)
def test_invalidCredentials(self):
"""
L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} when
passed a response which is not valid base64-encoded text.
"""
response = b64encode(b"123abc+/")
self.assertRaises(
error.LoginFailed,
self.credentialFactory.decode,
response,
self.makeRequest(),
)
class RequestMixin:
def makeRequest(self, method=b"GET", clientAddress=None):
"""
Create a L{DummyRequest} (change me to create a
L{twisted.web.http.Request} instead).
"""
if clientAddress is None:
clientAddress = IPv4Address("TCP", "localhost", 1234)
request = DummyRequest(b"/")
request.method = method
request.client = clientAddress
return request
class BasicAuthTests(RequestMixin, BasicAuthTestsMixin, unittest.TestCase):
"""
Basic authentication tests which use L{twisted.web.http.Request}.
"""
class DigestAuthTests(RequestMixin, unittest.TestCase):
"""
Digest authentication tests which use L{twisted.web.http.Request}.
"""
def setUp(self):
"""
Create a DigestCredentialFactory for testing
"""
self.realm = b"test realm"
self.algorithm = b"md5"
self.credentialFactory = digest.DigestCredentialFactory(
self.algorithm, self.realm
)
self.request = self.makeRequest()
def test_decode(self):
"""
L{digest.DigestCredentialFactory.decode} calls the C{decode} method on
L{twisted.cred.digest.DigestCredentialFactory} with the HTTP method and
host of the request.
"""
host = b"169.254.0.1"
method = b"GET"
done = [False]
response = object()
def check(_response, _method, _host):
self.assertEqual(response, _response)
self.assertEqual(method, _method)
self.assertEqual(host, _host)
done[0] = True
self.patch(self.credentialFactory.digest, "decode", check)
req = self.makeRequest(method, IPv4Address("TCP", host, 81))
self.credentialFactory.decode(response, req)
self.assertTrue(done[0])
def test_interface(self):
"""
L{DigestCredentialFactory} implements L{ICredentialFactory}.
"""
self.assertTrue(verifyObject(ICredentialFactory, self.credentialFactory))
def test_getChallenge(self):
"""
The challenge issued by L{DigestCredentialFactory.getChallenge} must
include C{'qop'}, C{'realm'}, C{'algorithm'}, C{'nonce'}, and
C{'opaque'} keys. The values for the C{'realm'} and C{'algorithm'}
keys must match the values supplied to the factory's initializer.
None of the values may have newlines in them.
"""
challenge = self.credentialFactory.getChallenge(self.request)
self.assertEqual(challenge["qop"], b"auth")
self.assertEqual(challenge["realm"], b"test realm")
self.assertEqual(challenge["algorithm"], b"md5")
self.assertIn("nonce", challenge)
self.assertIn("opaque", challenge)
for v in challenge.values():
self.assertNotIn(b"\n", v)
def test_getChallengeWithoutClientIP(self):
"""
L{DigestCredentialFactory.getChallenge} can issue a challenge even if
the L{Request} it is passed returns L{None} from C{getClientIP}.
"""
request = self.makeRequest(b"GET", None)
challenge = self.credentialFactory.getChallenge(request)
self.assertEqual(challenge["qop"], b"auth")
self.assertEqual(challenge["realm"], b"test realm")
self.assertEqual(challenge["algorithm"], b"md5")
self.assertIn("nonce", challenge)
self.assertIn("opaque", challenge)
class UnauthorizedResourceTests(RequestMixin, unittest.TestCase):
"""
Tests for L{UnauthorizedResource}.
"""
def test_getChildWithDefault(self):
"""
An L{UnauthorizedResource} is every child of itself.
"""
resource = UnauthorizedResource([])
self.assertIdentical(resource.getChildWithDefault("foo", None), resource)
self.assertIdentical(resource.getChildWithDefault("bar", None), resource)
def _unauthorizedRenderTest(self, request):
"""
Render L{UnauthorizedResource} for the given request object and verify
that the response code is I{Unauthorized} and that a I{WWW-Authenticate}
header is set in the response containing a challenge.
"""
resource = UnauthorizedResource([BasicCredentialFactory("example.com")])
request.render(resource)
self.assertEqual(request.responseCode, 401)
self.assertEqual(
request.responseHeaders.getRawHeaders(b"www-authenticate"),
[b'basic realm="example.com"'],
)
def test_render(self):
"""
L{UnauthorizedResource} renders with a 401 response code and a
I{WWW-Authenticate} header and puts a simple unauthorized message
into the response body.
"""
request = self.makeRequest()
self._unauthorizedRenderTest(request)
self.assertEqual(b"Unauthorized", b"".join(request.written))
def test_renderHEAD(self):
"""
The rendering behavior of L{UnauthorizedResource} for a I{HEAD} request
is like its handling of a I{GET} request, but no response body is
written.
"""
request = self.makeRequest(method=b"HEAD")
self._unauthorizedRenderTest(request)
self.assertEqual(b"", b"".join(request.written))
def test_renderQuotesRealm(self):
"""
The realm value included in the I{WWW-Authenticate} header set in
the response when L{UnauthorizedResounrce} is rendered has quotes
and backslashes escaped.
"""
resource = UnauthorizedResource([BasicCredentialFactory('example\\"foo')])
request = self.makeRequest()
request.render(resource)
self.assertEqual(
request.responseHeaders.getRawHeaders(b"www-authenticate"),
[b'basic realm="example\\\\\\"foo"'],
)
def test_renderQuotesDigest(self):
"""
The digest value included in the I{WWW-Authenticate} header
set in the response when L{UnauthorizedResource} is rendered
has quotes and backslashes escaped.
"""
resource = UnauthorizedResource(
[digest.DigestCredentialFactory(b"md5", b'example\\"foo')]
)
request = self.makeRequest()
request.render(resource)
authHeader = request.responseHeaders.getRawHeaders(b"www-authenticate")[0]
self.assertIn(b'realm="example\\\\\\"foo"', authHeader)
self.assertIn(b'hm="md5', authHeader)
implementer(portal.IRealm)
class Realm:
"""
A simple L{IRealm} implementation which gives out L{WebAvatar} for any
avatarId.
@type loggedIn: C{int}
@ivar loggedIn: The number of times C{requestAvatar} has been invoked for
L{IResource}.
@type loggedOut: C{int}
@ivar loggedOut: The number of times the logout callback has been invoked.
"""
def __init__(self, avatarFactory):
self.loggedOut = 0
self.loggedIn = 0
self.avatarFactory = avatarFactory
def requestAvatar(self, avatarId, mind, *interfaces):
if IResource in interfaces:
self.loggedIn += 1
return IResource, self.avatarFactory(avatarId), self.logout
raise NotImplementedError()
def logout(self):
self.loggedOut += 1
class HTTPAuthHeaderTests(unittest.TestCase):
"""
Tests for L{HTTPAuthSessionWrapper}.
"""
makeRequest = DummyRequest
def setUp(self):
"""
Create a realm, portal, and L{HTTPAuthSessionWrapper} to use in the tests.
"""
self.username = b"foo bar"
self.password = b"bar baz"
self.avatarContent = b"contents of the avatar resource itself"
self.childName = b"foo-child"
self.childContent = b"contents of the foo child of the avatar"
self.checker = InMemoryUsernamePasswordDatabaseDontUse()
self.checker.addUser(self.username, self.password)
self.avatar = Data(self.avatarContent, "text/plain")
self.avatar.putChild(self.childName, Data(self.childContent, "text/plain"))
self.avatars = {self.username: self.avatar}
self.realm = Realm(self.avatars.get)
self.portal = portal.Portal(self.realm, [self.checker])
self.credentialFactories = []
self.wrapper = HTTPAuthSessionWrapper(self.portal, self.credentialFactories)
def _authorizedBasicLogin(self, request):
"""
Add an I{basic authorization} header to the given request and then
dispatch it, starting from C{self.wrapper} and returning the resulting
L{IResource}.
"""
authorization = b64encode(self.username + b":" + self.password)
request.requestHeaders.addRawHeader(b"authorization", b"Basic " + authorization)
return getChildForRequest(self.wrapper, request)
def test_getChildWithDefault(self):
"""
Resource traversal which encounters an L{HTTPAuthSessionWrapper}
results in an L{UnauthorizedResource} instance when the request does
not have the required I{Authorization} headers.
"""
request = self.makeRequest([self.childName])
child = getChildForRequest(self.wrapper, request)
d = request.notifyFinish()
def cbFinished(result):
self.assertEqual(request.responseCode, 401)
d.addCallback(cbFinished)
request.render(child)
return d
def _invalidAuthorizationTest(self, response):
"""
Create a request with the given value as the value of an
I{Authorization} header and perform resource traversal with it,
starting at C{self.wrapper}. Assert that the result is a 401 response
code. Return a L{Deferred} which fires when this is all done.
"""
self.credentialFactories.append(BasicCredentialFactory("example.com"))
request = self.makeRequest([self.childName])
request.requestHeaders.addRawHeader(b"authorization", response)
child = getChildForRequest(self.wrapper, request)
d = request.notifyFinish()
def cbFinished(result):
self.assertEqual(request.responseCode, 401)
d.addCallback(cbFinished)
request.render(child)
return d
def test_getChildWithDefaultUnauthorizedUser(self):
"""
Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
results in an L{UnauthorizedResource} when the request has an
I{Authorization} header with a user which does not exist.
"""
return self._invalidAuthorizationTest(b"Basic " + b64encode(b"foo:bar"))
def test_getChildWithDefaultUnauthorizedPassword(self):
"""
Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
results in an L{UnauthorizedResource} when the request has an
I{Authorization} header with a user which exists and the wrong
password.
"""
return self._invalidAuthorizationTest(
b"Basic " + b64encode(self.username + b":bar")
)
def test_getChildWithDefaultUnrecognizedScheme(self):
"""
Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
results in an L{UnauthorizedResource} when the request has an
I{Authorization} header with an unrecognized scheme.
"""
return self._invalidAuthorizationTest(b"Quux foo bar baz")
def test_getChildWithDefaultAuthorized(self):
"""
Resource traversal which encounters an L{HTTPAuthSessionWrapper}
results in an L{IResource} which renders the L{IResource} avatar
retrieved from the portal when the request has a valid I{Authorization}
header.
"""
self.credentialFactories.append(BasicCredentialFactory("example.com"))
request = self.makeRequest([self.childName])
child = self._authorizedBasicLogin(request)
d = request.notifyFinish()
def cbFinished(ignored):
self.assertEqual(request.written, [self.childContent])
d.addCallback(cbFinished)
request.render(child)
return d
def test_renderAuthorized(self):
"""
Resource traversal which terminates at an L{HTTPAuthSessionWrapper}
and includes correct authentication headers results in the
L{IResource} avatar (not one of its children) retrieved from the
portal being rendered.
"""
self.credentialFactories.append(BasicCredentialFactory("example.com"))
# Request it exactly, not any of its children.
request = self.makeRequest([])
child = self._authorizedBasicLogin(request)
d = request.notifyFinish()
def cbFinished(ignored):
self.assertEqual(request.written, [self.avatarContent])
d.addCallback(cbFinished)
request.render(child)
return d
def test_getChallengeCalledWithRequest(self):
"""
When L{HTTPAuthSessionWrapper} finds an L{ICredentialFactory} to issue
a challenge, it calls the C{getChallenge} method with the request as an
argument.
"""
@implementer(ICredentialFactory)
class DumbCredentialFactory:
scheme = b"dumb"
def __init__(self):
self.requests = []
def getChallenge(self, request):
self.requests.append(request)
return {}
factory = DumbCredentialFactory()
self.credentialFactories.append(factory)
request = self.makeRequest([self.childName])
child = getChildForRequest(self.wrapper, request)
d = request.notifyFinish()
def cbFinished(ignored):
self.assertEqual(factory.requests, [request])
d.addCallback(cbFinished)
request.render(child)
return d
def _logoutTest(self):
"""
Issue a request for an authentication-protected resource using valid
credentials and then return the C{DummyRequest} instance which was
used.
This is a helper for tests about the behavior of the logout
callback.
"""
self.credentialFactories.append(BasicCredentialFactory("example.com"))
class SlowerResource(Resource):
def render(self, request):
return NOT_DONE_YET
self.avatar.putChild(self.childName, SlowerResource())
request = self.makeRequest([self.childName])
child = self._authorizedBasicLogin(request)
request.render(child)
self.assertEqual(self.realm.loggedOut, 0)
return request
def test_logout(self):
"""
The realm's logout callback is invoked after the resource is rendered.
"""
request = self._logoutTest()
request.finish()
self.assertEqual(self.realm.loggedOut, 1)
def test_logoutOnError(self):
"""
The realm's logout callback is also invoked if there is an error
generating the response (for example, if the client disconnects
early).
"""
request = self._logoutTest()
request.processingFailed(Failure(ConnectionDone("Simulated disconnect")))
self.assertEqual(self.realm.loggedOut, 1)
def test_decodeRaises(self):
"""
Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
results in an L{UnauthorizedResource} when the request has a I{Basic
Authorization} header which cannot be decoded using base64.
"""
self.credentialFactories.append(BasicCredentialFactory("example.com"))
request = self.makeRequest([self.childName])
request.requestHeaders.addRawHeader(
b"authorization", b"Basic decode should fail"
)
child = getChildForRequest(self.wrapper, request)
self.assertIsInstance(child, UnauthorizedResource)
def test_selectParseResponse(self):
"""
L{HTTPAuthSessionWrapper._selectParseHeader} returns a two-tuple giving
the L{ICredentialFactory} to use to parse the header and a string
containing the portion of the header which remains to be parsed.
"""
basicAuthorization = b"Basic abcdef123456"
self.assertEqual(
self.wrapper._selectParseHeader(basicAuthorization), (None, None)
)
factory = BasicCredentialFactory("example.com")
self.credentialFactories.append(factory)
self.assertEqual(
self.wrapper._selectParseHeader(basicAuthorization),
(factory, b"abcdef123456"),
)
def test_unexpectedDecodeError(self):
"""
Any unexpected exception raised by the credential factory's C{decode}
method results in a 500 response code and causes the exception to be
logged.
"""
logObserver = EventLoggingObserver.createWithCleanup(self, globalLogPublisher)
class UnexpectedException(Exception):
pass
class BadFactory:
scheme = b"bad"
def getChallenge(self, client):
return {}
def decode(self, response, request):
raise UnexpectedException()
self.credentialFactories.append(BadFactory())
request = self.makeRequest([self.childName])
request.requestHeaders.addRawHeader(b"authorization", b"Bad abc")
child = getChildForRequest(self.wrapper, request)
request.render(child)
self.assertEqual(request.responseCode, 500)
self.assertEquals(1, len(logObserver))
self.assertIsInstance(logObserver[0]["log_failure"].value, UnexpectedException)
self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
def test_unexpectedLoginError(self):
"""
Any unexpected failure from L{Portal.login} results in a 500 response
code and causes the failure to be logged.
"""
logObserver = EventLoggingObserver.createWithCleanup(self, globalLogPublisher)
class UnexpectedException(Exception):
pass
class BrokenChecker:
credentialInterfaces = (IUsernamePassword,)
def requestAvatarId(self, credentials):
raise UnexpectedException()
self.portal.registerChecker(BrokenChecker())
self.credentialFactories.append(BasicCredentialFactory("example.com"))
request = self.makeRequest([self.childName])
child = self._authorizedBasicLogin(request)
request.render(child)
self.assertEqual(request.responseCode, 500)
self.assertEquals(1, len(logObserver))
self.assertIsInstance(logObserver[0]["log_failure"].value, UnexpectedException)
self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
def test_anonymousAccess(self):
"""
Anonymous requests are allowed if a L{Portal} has an anonymous checker
registered.
"""
unprotectedContents = b"contents of the unprotected child resource"
self.avatars[ANONYMOUS] = Resource()
self.avatars[ANONYMOUS].putChild(
self.childName, Data(unprotectedContents, "text/plain")
)
self.portal.registerChecker(AllowAnonymousAccess())
self.credentialFactories.append(BasicCredentialFactory("example.com"))
request = self.makeRequest([self.childName])
child = getChildForRequest(self.wrapper, request)
d = request.notifyFinish()
def cbFinished(ignored):
self.assertEqual(request.written, [unprotectedContents])
d.addCallback(cbFinished)
request.render(child)
return d

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Test L{twisted.web.pages}
"""
from typing import cast
from twisted.trial.unittest import SynchronousTestCase
from twisted.web.http_headers import Headers
from twisted.web.iweb import IRequest
from twisted.web.pages import errorPage, forbidden, notFound
from twisted.web.resource import IResource
from twisted.web.test.requesthelper import DummyRequest
def _render(resource: IResource) -> DummyRequest:
"""
Render a response using the given resource.
@param resource: The resource to use to handle the request.
@returns: The request that the resource handled,
"""
request = DummyRequest([b""])
# The cast is necessary because DummyRequest isn't annotated
# as an IRequest, and this can't be trivially done. See
# https://github.com/twisted/twisted/issues/11719
resource.render(cast(IRequest, request))
return request
class ErrorPageTests(SynchronousTestCase):
"""
Test L{twisted.web.pages._ErrorPage} and its public aliases L{errorPage},
L{notFound} and L{forbidden}.
"""
maxDiff = None
def assertResponse(self, request: DummyRequest, code: int, body: bytes) -> None:
self.assertEqual(request.responseCode, code)
self.assertEqual(
request.responseHeaders,
Headers({b"content-type": [b"text/html; charset=utf-8"]}),
)
self.assertEqual(
# Decode to str because unittest somehow still doesn't diff bytes
# without truncating them in 2022.
b"".join(request.written).decode("latin-1"),
body.decode("latin-1"),
)
def test_escapesHTML(self) -> None:
"""
The I{brief} and I{detail} parameters are HTML-escaped on render.
"""
self.assertResponse(
_render(errorPage(400, "A & B", "<script>alert('oops!')")),
400,
(
b"<!DOCTYPE html>\n"
b"<html><head><title>400 - A &amp; B</title></head>"
b"<body><h1>A &amp; B</h1><p>&lt;script&gt;alert('oops!')"
b"</p></body></html>"
),
)
def test_getChild(self) -> None:
"""
The C{getChild} method of the resource returned by L{errorPage} returns
the L{_ErrorPage} it is called on.
"""
page = errorPage(404, "foo", "bar")
self.assertIs(
page.getChild(b"name", cast(IRequest, DummyRequest([b""]))),
page,
)
def test_notFoundDefaults(self) -> None:
"""
The default arguments to L{twisted.web.pages.notFound} produce
a reasonable error page.
"""
self.assertResponse(
_render(notFound()),
404,
(
b"<!DOCTYPE html>\n"
b"<html><head><title>404 - No Such Resource</title></head>"
b"<body><h1>No Such Resource</h1>"
b"<p>Sorry. No luck finding that resource.</p>"
b"</body></html>"
),
)
def test_forbiddenDefaults(self) -> None:
"""
The default arguments to L{twisted.web.pages.forbidden} produce
a reasonable error page.
"""
self.assertResponse(
_render(forbidden()),
403,
(
b"<!DOCTYPE html>\n"
b"<html><head><title>403 - Forbidden Resource</title></head>"
b"<body><h1>Forbidden Resource</h1>"
b"<p>Sorry, resource is forbidden.</p>"
b"</body></html>"
),
)

View File

@@ -0,0 +1,548 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Test for L{twisted.web.proxy}.
"""
from twisted.internet.testing import MemoryReactor, StringTransportWithDisconnection
from twisted.trial.unittest import TestCase
from twisted.web.proxy import (
ProxyClient,
ProxyClientFactory,
ProxyRequest,
ReverseProxyRequest,
ReverseProxyResource,
)
from twisted.web.resource import Resource
from twisted.web.server import Site
from twisted.web.test.test_web import DummyRequest
class ReverseProxyResourceTests(TestCase):
"""
Tests for L{ReverseProxyResource}.
"""
def _testRender(self, uri, expectedURI):
"""
Check that a request pointing at C{uri} produce a new proxy connection,
with the path of this request pointing at C{expectedURI}.
"""
root = Resource()
reactor = MemoryReactor()
resource = ReverseProxyResource("127.0.0.1", 1234, b"/path", reactor)
root.putChild(b"index", resource)
site = Site(root)
transport = StringTransportWithDisconnection()
channel = site.buildProtocol(None)
channel.makeConnection(transport)
# Clear the timeout if the tests failed
self.addCleanup(channel.connectionLost, None)
channel.dataReceived(b"GET " + uri + b" HTTP/1.1\r\nAccept: text/html\r\n\r\n")
[(host, port, factory, _timeout, _bind_addr)] = reactor.tcpClients
# Check that one connection has been created, to the good host/port
self.assertEqual(host, "127.0.0.1")
self.assertEqual(port, 1234)
# Check the factory passed to the connect, and its given path
self.assertIsInstance(factory, ProxyClientFactory)
self.assertEqual(factory.rest, expectedURI)
self.assertEqual(factory.headers[b"host"], b"127.0.0.1:1234")
def test_render(self):
"""
Test that L{ReverseProxyResource.render} initiates a connection to the
given server with a L{ProxyClientFactory} as parameter.
"""
return self._testRender(b"/index", b"/path")
def test_render_subpage(self):
"""
Test that L{ReverseProxyResource.render} will instantiate a child
resource that will initiate a connection to the given server
requesting the apropiate url subpath.
"""
return self._testRender(b"/index/page1", b"/path/page1")
def test_renderWithQuery(self):
"""
Test that L{ReverseProxyResource.render} passes query parameters to the
created factory.
"""
return self._testRender(b"/index?foo=bar", b"/path?foo=bar")
def test_getChild(self):
"""
The L{ReverseProxyResource.getChild} method should return a resource
instance with the same class as the originating resource, forward
port, host, and reactor values, and update the path value with the
value passed.
"""
reactor = MemoryReactor()
resource = ReverseProxyResource("127.0.0.1", 1234, b"/path", reactor)
child = resource.getChild(b"foo", None)
# The child should keep the same class
self.assertIsInstance(child, ReverseProxyResource)
self.assertEqual(child.path, b"/path/foo")
self.assertEqual(child.port, 1234)
self.assertEqual(child.host, "127.0.0.1")
self.assertIdentical(child.reactor, resource.reactor)
def test_getChildWithSpecial(self):
"""
The L{ReverseProxyResource} return by C{getChild} has a path which has
already been quoted.
"""
resource = ReverseProxyResource("127.0.0.1", 1234, b"/path")
child = resource.getChild(b" /%", None)
self.assertEqual(child.path, b"/path/%20%2F%25")
class DummyChannel:
"""
A dummy HTTP channel, that does nothing but holds a transport and saves
connection lost.
@ivar transport: the transport used by the client.
@ivar lostReason: the reason saved at connection lost.
"""
def __init__(self, transport):
"""
Hold a reference to the transport.
"""
self.transport = transport
self.lostReason = None
def connectionLost(self, reason):
"""
Keep track of the connection lost reason.
"""
self.lostReason = reason
def getPeer(self):
"""
Get peer information from the transport.
"""
return self.transport.getPeer()
def getHost(self):
"""
Get host information from the transport.
"""
return self.transport.getHost()
class ProxyClientTests(TestCase):
"""
Tests for L{ProxyClient}.
"""
def _parseOutHeaders(self, content):
"""
Parse the headers out of some web content.
@param content: Bytes received from a web server.
@return: A tuple of (requestLine, headers, body). C{headers} is a dict
of headers, C{requestLine} is the first line (e.g. "POST /foo ...")
and C{body} is whatever is left.
"""
headers, body = content.split(b"\r\n\r\n")
headers = headers.split(b"\r\n")
requestLine = headers.pop(0)
return (requestLine, dict(header.split(b": ") for header in headers), body)
def makeRequest(self, path):
"""
Make a dummy request object for the URL path.
@param path: A URL path, beginning with a slash.
@return: A L{DummyRequest}.
"""
return DummyRequest(path)
def makeProxyClient(self, request, method=b"GET", headers=None, requestBody=b""):
"""
Make a L{ProxyClient} object used for testing.
@param request: The request to use.
@param method: The HTTP method to use, GET by default.
@param headers: The HTTP headers to use expressed as a dict. If not
provided, defaults to {'accept': 'text/html'}.
@param requestBody: The body of the request. Defaults to the empty
string.
@return: A L{ProxyClient}
"""
if headers is None:
headers = {b"accept": b"text/html"}
path = b"/" + request.postpath
return ProxyClient(method, path, b"HTTP/1.0", headers, requestBody, request)
def connectProxy(self, proxyClient):
"""
Connect a proxy client to a L{StringTransportWithDisconnection}.
@param proxyClient: A L{ProxyClient}.
@return: The L{StringTransportWithDisconnection}.
"""
clientTransport = StringTransportWithDisconnection()
clientTransport.protocol = proxyClient
proxyClient.makeConnection(clientTransport)
return clientTransport
def assertForwardsHeaders(self, proxyClient, requestLine, headers):
"""
Assert that C{proxyClient} sends C{headers} when it connects.
@param proxyClient: A L{ProxyClient}.
@param requestLine: The request line we expect to be sent.
@param headers: A dict of headers we expect to be sent.
@return: If the assertion is successful, return the request body as
bytes.
"""
self.connectProxy(proxyClient)
requestContent = proxyClient.transport.value()
receivedLine, receivedHeaders, body = self._parseOutHeaders(requestContent)
self.assertEqual(receivedLine, requestLine)
self.assertEqual(receivedHeaders, headers)
return body
def makeResponseBytes(self, code, message, headers, body):
lines = [b"HTTP/1.0 " + str(code).encode("ascii") + b" " + message]
for header, values in headers:
for value in values:
lines.append(header + b": " + value)
lines.extend([b"", body])
return b"\r\n".join(lines)
def assertForwardsResponse(self, request, code, message, headers, body):
"""
Assert that C{request} has forwarded a response from the server.
@param request: A L{DummyRequest}.
@param code: The expected HTTP response code.
@param message: The expected HTTP message.
@param headers: The expected HTTP headers.
@param body: The expected response body.
"""
self.assertEqual(request.responseCode, code)
self.assertEqual(request.responseMessage, message)
receivedHeaders = list(request.responseHeaders.getAllRawHeaders())
receivedHeaders.sort()
expectedHeaders = headers[:]
expectedHeaders.sort()
self.assertEqual(receivedHeaders, expectedHeaders)
self.assertEqual(b"".join(request.written), body)
def _testDataForward(
self,
code,
message,
headers,
body,
method=b"GET",
requestBody=b"",
loseConnection=True,
):
"""
Build a fake proxy connection, and send C{data} over it, checking that
it's forwarded to the originating request.
"""
request = self.makeRequest(b"foo")
client = self.makeProxyClient(
request, method, {b"accept": b"text/html"}, requestBody
)
receivedBody = self.assertForwardsHeaders(
client,
method + b" /foo HTTP/1.0",
{b"connection": b"close", b"accept": b"text/html"},
)
self.assertEqual(receivedBody, requestBody)
# Fake an answer
client.dataReceived(self.makeResponseBytes(code, message, headers, body))
# Check that the response data has been forwarded back to the original
# requester.
self.assertForwardsResponse(request, code, message, headers, body)
# Check that when the response is done, the request is finished.
if loseConnection:
client.transport.loseConnection()
# Even if we didn't call loseConnection, the transport should be
# disconnected. This lets us not rely on the server to close our
# sockets for us.
self.assertFalse(client.transport.connected)
self.assertEqual(request.finished, 1)
def test_forward(self):
"""
When connected to the server, L{ProxyClient} should send the saved
request, with modifications of the headers, and then forward the result
to the parent request.
"""
return self._testDataForward(
200, b"OK", [(b"Foo", [b"bar", b"baz"])], b"Some data\r\n"
)
def test_postData(self):
"""
Try to post content in the request, and check that the proxy client
forward the body of the request.
"""
return self._testDataForward(
200, b"OK", [(b"Foo", [b"bar"])], b"Some data\r\n", b"POST", b"Some content"
)
def test_statusWithMessage(self):
"""
If the response contains a status with a message, it should be
forwarded to the parent request with all the information.
"""
return self._testDataForward(404, b"Not Found", [], b"")
def test_contentLength(self):
"""
If the response contains a I{Content-Length} header, the inbound
request object should still only have C{finish} called on it once.
"""
data = b"foo bar baz"
return self._testDataForward(
200, b"OK", [(b"Content-Length", [str(len(data)).encode("ascii")])], data
)
def test_losesConnection(self):
"""
If the response contains a I{Content-Length} header, the outgoing
connection is closed when all response body data has been received.
"""
data = b"foo bar baz"
return self._testDataForward(
200,
b"OK",
[(b"Content-Length", [str(len(data)).encode("ascii")])],
data,
loseConnection=False,
)
def test_headersCleanups(self):
"""
The headers given at initialization should be modified:
B{proxy-connection} should be removed if present, and B{connection}
should be added.
"""
client = ProxyClient(
b"GET",
b"/foo",
b"HTTP/1.0",
{b"accept": b"text/html", b"proxy-connection": b"foo"},
b"",
None,
)
self.assertEqual(
client.headers, {b"accept": b"text/html", b"connection": b"close"}
)
def test_keepaliveNotForwarded(self):
"""
The proxy doesn't really know what to do with keepalive things from
the remote server, so we stomp over any keepalive header we get from
the client.
"""
headers = {
b"accept": b"text/html",
b"keep-alive": b"300",
b"connection": b"keep-alive",
}
expectedHeaders = headers.copy()
expectedHeaders[b"connection"] = b"close"
del expectedHeaders[b"keep-alive"]
client = ProxyClient(b"GET", b"/foo", b"HTTP/1.0", headers, b"", None)
self.assertForwardsHeaders(client, b"GET /foo HTTP/1.0", expectedHeaders)
def test_defaultHeadersOverridden(self):
"""
L{server.Request} within the proxy sets certain response headers by
default. When we get these headers back from the remote server, the
defaults are overridden rather than simply appended.
"""
request = self.makeRequest(b"foo")
request.responseHeaders.setRawHeaders(b"server", [b"old-bar"])
request.responseHeaders.setRawHeaders(b"date", [b"old-baz"])
request.responseHeaders.setRawHeaders(b"content-type", [b"old/qux"])
client = self.makeProxyClient(request, headers={b"accept": b"text/html"})
self.connectProxy(client)
headers = {
b"Server": [b"bar"],
b"Date": [b"2010-01-01"],
b"Content-Type": [b"application/x-baz"],
}
client.dataReceived(self.makeResponseBytes(200, b"OK", headers.items(), b""))
self.assertForwardsResponse(request, 200, b"OK", list(headers.items()), b"")
class ProxyClientFactoryTests(TestCase):
"""
Tests for L{ProxyClientFactory}.
"""
def test_connectionFailed(self):
"""
Check that L{ProxyClientFactory.clientConnectionFailed} produces
a B{501} response to the parent request.
"""
request = DummyRequest([b"foo"])
factory = ProxyClientFactory(
b"GET", b"/foo", b"HTTP/1.0", {b"accept": b"text/html"}, "", request
)
factory.clientConnectionFailed(None, None)
self.assertEqual(request.responseCode, 501)
self.assertEqual(request.responseMessage, b"Gateway error")
self.assertEqual(
list(request.responseHeaders.getAllRawHeaders()),
[(b"Content-Type", [b"text/html"])],
)
self.assertEqual(b"".join(request.written), b"<H1>Could not connect</H1>")
self.assertEqual(request.finished, 1)
def test_buildProtocol(self):
"""
L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient}
with the same values of attributes (with updates on the headers).
"""
factory = ProxyClientFactory(
b"GET", b"/foo", b"HTTP/1.0", {b"accept": b"text/html"}, b"Some data", None
)
proto = factory.buildProtocol(None)
self.assertIsInstance(proto, ProxyClient)
self.assertEqual(proto.command, b"GET")
self.assertEqual(proto.rest, b"/foo")
self.assertEqual(proto.data, b"Some data")
self.assertEqual(
proto.headers, {b"accept": b"text/html", b"connection": b"close"}
)
class ProxyRequestTests(TestCase):
"""
Tests for L{ProxyRequest}.
"""
def _testProcess(self, uri, expectedURI, method=b"GET", data=b""):
"""
Build a request pointing at C{uri}, and check that a proxied request
is created, pointing a C{expectedURI}.
"""
transport = StringTransportWithDisconnection()
channel = DummyChannel(transport)
reactor = MemoryReactor()
request = ProxyRequest(channel, False, reactor)
request.gotLength(len(data))
request.handleContentChunk(data)
request.requestReceived(method, b"http://example.com" + uri, b"HTTP/1.0")
self.assertEqual(len(reactor.tcpClients), 1)
self.assertEqual(reactor.tcpClients[0][0], "example.com")
self.assertEqual(reactor.tcpClients[0][1], 80)
factory = reactor.tcpClients[0][2]
self.assertIsInstance(factory, ProxyClientFactory)
self.assertEqual(factory.command, method)
self.assertEqual(factory.version, b"HTTP/1.0")
self.assertEqual(factory.headers, {b"host": b"example.com"})
self.assertEqual(factory.data, data)
self.assertEqual(factory.rest, expectedURI)
self.assertEqual(factory.father, request)
def test_process(self):
"""
L{ProxyRequest.process} should create a connection to the given server,
with a L{ProxyClientFactory} as connection factory, with the correct
parameters:
- forward comment, version and data values
- update headers with the B{host} value
- remove the host from the URL
- pass the request as parent request
"""
return self._testProcess(b"/foo/bar", b"/foo/bar")
def test_processWithoutTrailingSlash(self):
"""
If the incoming request doesn't contain a slash,
L{ProxyRequest.process} should add one when instantiating
L{ProxyClientFactory}.
"""
return self._testProcess(b"", b"/")
def test_processWithData(self):
"""
L{ProxyRequest.process} should be able to retrieve request body and
to forward it.
"""
return self._testProcess(b"/foo/bar", b"/foo/bar", b"POST", b"Some content")
def test_processWithPort(self):
"""
Check that L{ProxyRequest.process} correctly parse port in the incoming
URL, and create an outgoing connection with this port.
"""
transport = StringTransportWithDisconnection()
channel = DummyChannel(transport)
reactor = MemoryReactor()
request = ProxyRequest(channel, False, reactor)
request.gotLength(0)
request.requestReceived(b"GET", b"http://example.com:1234/foo/bar", b"HTTP/1.0")
# That should create one connection, with the port parsed from the URL
self.assertEqual(len(reactor.tcpClients), 1)
self.assertEqual(reactor.tcpClients[0][0], "example.com")
self.assertEqual(reactor.tcpClients[0][1], 1234)
class DummyFactory:
"""
A simple holder for C{host} and C{port} information.
"""
def __init__(self, host, port):
self.host = host
self.port = port
class ReverseProxyRequestTests(TestCase):
"""
Tests for L{ReverseProxyRequest}.
"""
def test_process(self):
"""
L{ReverseProxyRequest.process} should create a connection to its
factory host/port, using a L{ProxyClientFactory} instantiated with the
correct parameters, and particularly set the B{host} header to the
factory host.
"""
transport = StringTransportWithDisconnection()
channel = DummyChannel(transport)
reactor = MemoryReactor()
request = ReverseProxyRequest(channel, False, reactor)
request.factory = DummyFactory("example.com", 1234)
request.gotLength(0)
request.requestReceived(b"GET", b"/foo/bar", b"HTTP/1.0")
# Check that one connection has been created, to the good host/port
self.assertEqual(len(reactor.tcpClients), 1)
self.assertEqual(reactor.tcpClients[0][0], "example.com")
self.assertEqual(reactor.tcpClients[0][1], 1234)
# Check the factory passed to the connect, and its headers
factory = reactor.tcpClients[0][2]
self.assertIsInstance(factory, ProxyClientFactory)
self.assertEqual(factory.headers, {b"host": b"example.com"})

View File

@@ -0,0 +1,300 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.resource}.
"""
from twisted.trial.unittest import TestCase
from twisted.web.error import UnsupportedMethod
from twisted.web.http_headers import Headers
from twisted.web.iweb import IRequest
from twisted.web.resource import (
FORBIDDEN,
NOT_FOUND,
Resource,
_UnsafeErrorPage as ErrorPage,
_UnsafeForbiddenResource as ForbiddenResource,
_UnsafeNoResource as NoResource,
getChildForRequest,
)
from twisted.web.test.requesthelper import DummyRequest
class ErrorPageTests(TestCase):
"""
Tests for L{_UnafeErrorPage}, L{_UnsafeNoResource}, and
L{_UnsafeForbiddenResource}.
"""
def test_deprecatedErrorPage(self) -> None:
"""
The public C{twisted.web.resource.ErrorPage} alias for the
corresponding C{_Unsafe} class produces a deprecation warning when
called.
"""
_ = ErrorPage(123, "ono", "ono!")
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
self.assertIn("twisted.web.pages.errorPage", warning["message"])
def test_deprecatedNoResource(self) -> None:
"""
The public C{twisted.web.resource.NoResource} alias for the
corresponding C{_Unsafe} class produces a deprecation warning when
called.
"""
_ = NoResource()
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
self.assertIn("twisted.web.pages.notFound", warning["message"])
def test_deprecatedForbiddenResource(self) -> None:
"""
The public C{twisted.web.resource.ForbiddenResource} alias for the
corresponding C{_Unsafe} class produce a deprecation warning when
called.
"""
_ = ForbiddenResource()
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
self.assertIn("twisted.web.pages.forbidden", warning["message"])
def test_getChild(self) -> None:
"""
The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
called on.
"""
page = ErrorPage(321, "foo", "bar")
self.assertIdentical(page.getChild(b"name", object()), page)
def _pageRenderingTest(
self, page: Resource, code: int, brief: str, detail: str
) -> None:
request = DummyRequest([b""])
template = (
"\n"
"<html>\n"
" <head><title>%s - %s</title></head>\n"
" <body>\n"
" <h1>%s</h1>\n"
" <p>%s</p>\n"
" </body>\n"
"</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) -> None:
"""
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 = ErrorPage(code, brief, detail)
self._pageRenderingTest(page, code, brief, detail)
def test_noResourceRendering(self) -> None:
"""
L{NoResource} sets the HTTP I{NOT FOUND} code.
"""
detail = "long message"
page = NoResource(detail)
self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)
def test_forbiddenResourceRendering(self) -> None:
"""
L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
"""
detail = "longer message"
page = 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: bytes, request: IRequest) -> None:
Resource.__init__(self)
self.path = path
self.request = request
class DynamicChildren(Resource):
"""
A L{Resource} with dynamic children.
"""
def getChild(self, path: bytes, request: IRequest) -> DynamicChild:
return DynamicChild(path, request)
class BytesReturnedRenderable(Resource):
"""
A L{Resource} with minimal capabilities to render a response.
"""
def __init__(self, response: bytes) -> None:
"""
@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: object) -> bytes:
"""
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: object) -> None:
pass
def render_PUT(self, request: object) -> None:
pass
class ResourceTests(TestCase):
"""
Tests for L{Resource}.
"""
def test_staticChildren(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
Test that passing the wrong type to putChild results in a warning,
and a failure in Python 3
"""
resource = Resource()
child = Resource()
sibling = Resource()
self.assertRaises(TypeError, resource.putChild, "foo", child)
self.assertRaises(TypeError, resource.putChild, None, sibling)
def test_defaultHEAD(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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 = {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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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"])

View File

@@ -0,0 +1,121 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.script}.
"""
import os
from twisted.internet import defer
from twisted.python.filepath import FilePath
from twisted.trial.unittest import TestCase
from twisted.web.http import NOT_FOUND
from twisted.web.script import PythonScript, ResourceScriptDirectory
from twisted.web.test._util import _render
from twisted.web.test.requesthelper import DummyRequest
class ResourceScriptDirectoryTests(TestCase):
"""
Tests for L{ResourceScriptDirectory}.
"""
def test_renderNotFound(self) -> defer.Deferred[None]:
"""
L{ResourceScriptDirectory.render} sets the HTTP response code to I{NOT
FOUND}.
"""
resource = ResourceScriptDirectory(self.mktemp())
request = DummyRequest([b""])
d = _render(resource, request)
def cbRendered(ignored: object) -> None:
self.assertEqual(request.responseCode, NOT_FOUND)
return d.addCallback(cbRendered)
def test_notFoundChild(self) -> defer.Deferred[None]:
"""
L{ResourceScriptDirectory.getChild} returns a resource which renders an
response with the HTTP I{NOT FOUND} status code if the indicated child
does not exist as an entry in the directory used to initialized the
L{ResourceScriptDirectory}.
"""
path = self.mktemp()
os.makedirs(path)
resource = ResourceScriptDirectory(path)
request = DummyRequest([b"foo"])
child = resource.getChild("foo", request)
d = _render(child, request)
def cbRendered(ignored: object) -> None:
self.assertEqual(request.responseCode, NOT_FOUND)
return d.addCallback(cbRendered)
def test_render(self) -> defer.Deferred[None]:
"""
L{ResourceScriptDirectory.getChild} returns a resource which renders a
response with the HTTP 200 status code and the content of the rpy's
C{request} global.
"""
tmp = FilePath(self.mktemp())
tmp.makedirs()
tmp.child("test.rpy").setContent(
b"""
from twisted.web.resource import Resource
class TestResource(Resource):
isLeaf = True
def render_GET(self, request):
return b'ok'
resource = TestResource()"""
)
resource = ResourceScriptDirectory(tmp._asBytesPath())
request = DummyRequest([b""])
child = resource.getChild(b"test.rpy", request)
d = _render(child, request)
def cbRendered(ignored: object) -> None:
self.assertEqual(b"".join(request.written), b"ok")
return d.addCallback(cbRendered)
class PythonScriptTests(TestCase):
"""
Tests for L{PythonScript}.
"""
def test_notFoundRender(self) -> defer.Deferred[None]:
"""
If the source file a L{PythonScript} is initialized with doesn't exist,
L{PythonScript.render} sets the HTTP response code to I{NOT FOUND}.
"""
resource = PythonScript(self.mktemp(), None)
request = DummyRequest([b""])
d = _render(resource, request)
def cbRendered(ignored: object) -> None:
self.assertEqual(request.responseCode, NOT_FOUND)
return d.addCallback(cbRendered)
def test_renderException(self) -> defer.Deferred[None]:
"""
L{ResourceScriptDirectory.getChild} returns a resource which renders a
response with the HTTP 200 status code and the content of the rpy's
C{request} global.
"""
tmp = FilePath(self.mktemp())
tmp.makedirs()
child = tmp.child("test.epy")
child.setContent(b'raise Exception("nooo")')
resource = PythonScript(child._asBytesPath(), None)
request = DummyRequest([b""])
d = _render(resource, request)
def cbRendered(ignored: object) -> None:
self.assertIn(b"nooo", b"".join(request.written))
return d.addCallback(cbRendered)

View File

@@ -0,0 +1,197 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web._stan} portion of the L{twisted.web.template}
implementation.
"""
import sys
from typing import NoReturn
from twisted.trial.unittest import TestCase
from twisted.web.template import CDATA, CharRef, Comment, Flattenable, Tag
def proto(*a: Flattenable, **kw: Flattenable) -> Tag:
"""
Produce a new tag for testing.
"""
return Tag("hello")(*a, **kw)
class TagTests(TestCase):
"""
Tests for L{Tag}.
"""
def test_renderAttribute(self) -> None:
"""
Setting an attribute named C{render} will change the C{render} instance
variable instead of adding an attribute.
"""
tag = proto(render="myRenderer")
self.assertEqual(tag.render, "myRenderer")
self.assertEqual(tag.attributes, {})
def test_renderAttributeNonString(self) -> None:
"""
Attempting to set an attribute named C{render} to something other than
a string will raise L{TypeError}.
"""
with self.assertRaises(TypeError) as e:
proto(render=83) # type: ignore[arg-type]
self.assertEqual(
e.exception.args[0], 'Value for "render" attribute must be str, got 83'
)
def test_fillSlots(self) -> None:
"""
L{Tag.fillSlots} returns self.
"""
tag = proto()
self.assertIdentical(tag, tag.fillSlots(test="test"))
def test_cloneShallow(self) -> None:
"""
L{Tag.clone} copies all attributes and children of a tag, including its
render attribute. If the shallow flag is C{False}, that's where it
stops.
"""
innerList = ["inner list"]
tag = proto("How are you", innerList, hello="world", render="aSampleMethod")
tag.fillSlots(foo="bar")
tag.filename = "foo/bar"
tag.lineNumber = 6
tag.columnNumber = 12
clone = tag.clone(deep=False)
self.assertEqual(clone.attributes["hello"], "world")
self.assertNotIdentical(clone.attributes, tag.attributes)
self.assertEqual(clone.children, ["How are you", innerList])
self.assertNotIdentical(clone.children, tag.children)
self.assertIdentical(clone.children[1], innerList)
self.assertEqual(tag.slotData, clone.slotData)
self.assertNotIdentical(tag.slotData, clone.slotData)
self.assertEqual(clone.filename, "foo/bar")
self.assertEqual(clone.lineNumber, 6)
self.assertEqual(clone.columnNumber, 12)
self.assertEqual(clone.render, "aSampleMethod")
def test_cloneDeep(self) -> None:
"""
L{Tag.clone} copies all attributes and children of a tag, including its
render attribute. In its normal operating mode (where the deep flag is
C{True}, as is the default), it will clone all sub-lists and sub-tags.
"""
innerTag = proto("inner")
innerList = ["inner list"]
tag = proto(
"How are you", innerTag, innerList, hello="world", render="aSampleMethod"
)
tag.fillSlots(foo="bar")
tag.filename = "foo/bar"
tag.lineNumber = 6
tag.columnNumber = 12
clone = tag.clone()
self.assertEqual(clone.attributes["hello"], "world")
self.assertNotIdentical(clone.attributes, tag.attributes)
self.assertNotIdentical(clone.children, tag.children)
# sanity check
self.assertIdentical(tag.children[1], innerTag)
# clone should have sub-clone
self.assertNotIdentical(clone.children[1], innerTag)
# sanity check
self.assertIdentical(tag.children[2], innerList)
# clone should have sub-clone
self.assertNotIdentical(clone.children[2], innerList)
self.assertEqual(tag.slotData, clone.slotData)
self.assertNotIdentical(tag.slotData, clone.slotData)
self.assertEqual(clone.filename, "foo/bar")
self.assertEqual(clone.lineNumber, 6)
self.assertEqual(clone.columnNumber, 12)
self.assertEqual(clone.render, "aSampleMethod")
def test_cloneGeneratorDeprecation(self) -> None:
"""
Cloning a tag containing a generator is unsafe. To avoid breaking
programs that only flatten the clone or only flatten the original,
we deprecate old behavior rather than making it an error immediately.
"""
tag = proto(str(n) for n in range(10))
self.assertWarns(
DeprecationWarning,
"Cloning a Tag which contains a generator is unsafe, "
"since the generator can be consumed only once; "
"this is deprecated since Twisted 21.7.0 and will raise "
"an exception in the future",
sys.modules[Tag.__module__].__file__,
tag.clone,
)
def test_cloneCoroutineDeprecation(self) -> None:
"""
Cloning a tag containing a coroutine is unsafe. To avoid breaking
programs that only flatten the clone or only flatten the original,
we deprecate old behavior rather than making it an error immediately.
"""
async def asyncFunc() -> NoReturn:
raise NotImplementedError
coro = asyncFunc()
tag = proto("123", coro, "789")
try:
self.assertWarns(
DeprecationWarning,
"Cloning a Tag which contains a coroutine is unsafe, "
"since the coroutine can run only once; "
"this is deprecated since Twisted 21.7.0 and will raise "
"an exception in the future",
sys.modules[Tag.__module__].__file__,
tag.clone,
)
finally:
coro.close()
def test_clear(self) -> None:
"""
L{Tag.clear} removes all children from a tag, but leaves its attributes
in place.
"""
tag = proto("these are", "children", "cool", andSoIs="this-attribute")
tag.clear()
self.assertEqual(tag.children, [])
self.assertEqual(tag.attributes, {"andSoIs": "this-attribute"})
def test_suffix(self) -> None:
"""
L{Tag.__call__} accepts Python keywords with a suffixed underscore as
the DOM attribute of that literal suffix.
"""
proto = Tag("div")
tag = proto()
tag(class_="a")
self.assertEqual(tag.attributes, {"class": "a"})
def test_commentReprPy3(self) -> None:
"""
L{Comment.__repr__} returns a value which makes it easy to see what's
in the comment.
"""
self.assertEqual(repr(Comment("hello there")), "Comment('hello there')")
def test_cdataReprPy3(self) -> None:
"""
L{CDATA.__repr__} returns a value which makes it easy to see what's in
the comment.
"""
self.assertEqual(repr(CDATA("test data")), "CDATA('test data')")
def test_charrefRepr(self) -> None:
"""
L{CharRef.__repr__} returns a value which makes it easy to see what
character is referred to.
"""
snowman = ord("\N{SNOWMAN}")
self.assertEqual(repr(CharRef(snowman)), "CharRef(9731)")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,319 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.tap}.
"""
from __future__ import annotations
import os
import stat
from typing import cast
from unittest import skipIf
from twisted.internet import endpoints, reactor
from twisted.internet.interfaces import IReactorCore, IReactorUNIX
from twisted.python.filepath import FilePath
from twisted.python.reflect import requireModule
from twisted.python.threadpool import ThreadPool
from twisted.python.usage import UsageError
from twisted.spread.pb import PBServerFactory
from twisted.trial.unittest import TestCase
from twisted.web import demo
from twisted.web.distrib import ResourcePublisher, UserDirectory
from twisted.web.script import PythonScript
from twisted.web.server import Site
from twisted.web.static import Data, File
from twisted.web.tap import (
Options,
_AddHeadersResource,
makePersonalServerFactory,
makeService,
)
from twisted.web.test.requesthelper import DummyRequest
from twisted.web.twcgi import CGIScript
from twisted.web.wsgi import WSGIResource
application = object()
class ServiceTests(TestCase):
"""
Tests for the service creation APIs in L{twisted.web.tap}.
"""
def _pathOption(self) -> tuple[FilePath[str], File]:
"""
Helper for the I{--path} tests which creates a directory and creates
an L{Options} object which uses that directory as its static
filesystem root.
@return: A two-tuple of a L{FilePath} referring to the directory and
the value associated with the C{'root'} key in the L{Options}
instance after parsing a I{--path} option.
"""
path = FilePath(self.mktemp())
path.makedirs()
options = Options()
options.parseOptions(["--path", path.path])
root = options["root"]
return path, root
def test_path(self) -> None:
"""
The I{--path} option causes L{Options} to create a root resource
which serves responses from the specified path.
"""
path, root = self._pathOption()
self.assertIsInstance(root, File)
self.assertEqual(root.path, path.path)
@skipIf(
not IReactorUNIX.providedBy(reactor),
"The reactor does not support UNIX domain sockets",
)
def test_pathServer(self) -> None:
"""
The I{--path} option to L{makeService} causes it to return a service
which will listen on the server address given by the I{--port} option.
"""
path = FilePath(self.mktemp())
path.makedirs()
port = self.mktemp()
options = Options()
options.parseOptions(["--port", "unix:" + port, "--path", path.path])
service = makeService(options)
service.startService()
self.addCleanup(service.stopService)
self.assertIsInstance(service.services[0].factory.resource, File)
self.assertEqual(service.services[0].factory.resource.path, path.path)
self.assertTrue(os.path.exists(port))
self.assertTrue(stat.S_ISSOCK(os.stat(port).st_mode))
def test_cgiProcessor(self) -> None:
"""
The I{--path} option creates a root resource which serves a
L{CGIScript} instance for any child with the C{".cgi"} extension.
"""
path, root = self._pathOption()
path.child("foo.cgi").setContent(b"")
self.assertIsInstance(root.getChild("foo.cgi", None), CGIScript)
def test_epyProcessor(self) -> None:
"""
The I{--path} option creates a root resource which serves a
L{PythonScript} instance for any child with the C{".epy"} extension.
"""
path, root = self._pathOption()
path.child("foo.epy").setContent(b"")
self.assertIsInstance(root.getChild("foo.epy", None), PythonScript)
def test_rpyProcessor(self) -> None:
"""
The I{--path} option creates a root resource which serves the
C{resource} global defined by the Python source in any child with
the C{".rpy"} extension.
"""
path, root = self._pathOption()
path.child("foo.rpy").setContent(
b"from twisted.web.static import Data\n"
b"resource = Data('content', 'major/minor')\n"
)
child = root.getChild("foo.rpy", None)
self.assertIsInstance(child, Data)
self.assertEqual(child.data, "content")
self.assertEqual(child.type, "major/minor")
def test_makePersonalServerFactory(self) -> None:
"""
L{makePersonalServerFactory} returns a PB server factory which has
as its root object a L{ResourcePublisher}.
"""
# The fact that this pile of objects can actually be used somehow is
# verified by twisted.web.test.test_distrib.
site = Site(Data(b"foo bar", "text/plain"))
serverFactory = makePersonalServerFactory(site)
self.assertIsInstance(serverFactory, PBServerFactory)
self.assertIsInstance(serverFactory.root, ResourcePublisher)
self.assertIdentical(serverFactory.root.site, site)
@skipIf(
not IReactorUNIX.providedBy(reactor),
"The reactor does not support UNIX domain sockets",
)
def test_personalServer(self) -> None:
"""
The I{--personal} option to L{makeService} causes it to return a
service which will listen on the server address given by the I{--port}
option.
"""
port = self.mktemp()
options = Options()
options.parseOptions(["--port", "unix:" + port, "--personal"])
service = makeService(options)
service.startService()
self.addCleanup(service.stopService)
self.assertTrue(os.path.exists(port))
self.assertTrue(stat.S_ISSOCK(os.stat(port).st_mode))
@skipIf(
not IReactorUNIX.providedBy(reactor),
"The reactor does not support UNIX domain sockets",
)
def test_defaultPersonalPath(self) -> None:
"""
If the I{--port} option not specified but the I{--personal} option is,
L{Options} defaults the port to C{UserDirectory.userSocketName} in the
user's home directory.
"""
options = Options()
options.parseOptions(["--personal"])
path = os.path.expanduser(os.path.join("~", UserDirectory.userSocketName))
self.assertEqual(options["ports"][0], f"unix:{path}")
def test_defaultPort(self) -> None:
"""
If the I{--port} option is not specified, L{Options} defaults the port
to C{8080}.
"""
options = Options()
options.parseOptions([])
self.assertEqual(
endpoints._parseServer(options["ports"][0], None)[:2], ("TCP", (8080, None))
)
def test_twoPorts(self) -> None:
"""
If the I{--http} option is given twice, there are two listeners
"""
options = Options()
options.parseOptions(["--listen", "tcp:8001", "--listen", "tcp:8002"])
self.assertIn("8001", options["ports"][0])
self.assertIn("8002", options["ports"][1])
def test_wsgi(self) -> None:
"""
The I{--wsgi} option takes the fully-qualifed Python name of a WSGI
application object and creates a L{WSGIResource} at the root which
serves that application.
"""
options = Options()
options.parseOptions(["--wsgi", __name__ + ".application"])
root = options["root"]
self.assertTrue(root, WSGIResource)
self.assertIdentical(root._reactor, reactor)
self.assertTrue(isinstance(root._threadpool, ThreadPool))
self.assertIdentical(root._application, application)
# The threadpool should start and stop with the reactor.
self.assertFalse(root._threadpool.started)
cast(IReactorCore, reactor).fireSystemEvent("startup")
self.assertTrue(root._threadpool.started)
self.assertFalse(root._threadpool.joined)
cast(IReactorCore, reactor).fireSystemEvent("shutdown")
self.assertTrue(root._threadpool.joined)
def test_invalidApplication(self) -> None:
"""
If I{--wsgi} is given an invalid name, L{Options.parseOptions}
raises L{UsageError}.
"""
options = Options()
for name in [__name__ + ".nosuchthing", "foo."]:
exc = self.assertRaises(UsageError, options.parseOptions, ["--wsgi", name])
self.assertEqual(str(exc), f"No such WSGI application: {name!r}")
@skipIf(requireModule("OpenSSL.SSL") is not None, "SSL module is available.")
def test_HTTPSFailureOnMissingSSL(self) -> None:
"""
An L{UsageError} is raised when C{https} is requested but there is no
support for SSL.
"""
options = Options()
exception = self.assertRaises(UsageError, options.parseOptions, ["--https=443"])
self.assertEqual("SSL support not installed", exception.args[0])
@skipIf(requireModule("OpenSSL.SSL") is None, "SSL module is not available.")
def test_HTTPSAcceptedOnAvailableSSL(self) -> None:
"""
When SSL support is present, it accepts the --https option.
"""
options = Options()
options.parseOptions(["--https=443"])
self.assertIn("ssl", options["ports"][0])
self.assertIn("443", options["ports"][0])
def test_add_header_parsing(self) -> None:
"""
When --add-header is specific, the value is parsed.
"""
options = Options()
options.parseOptions(["--add-header", "K1: V1", "--add-header", "K2: V2"])
self.assertEqual(options["extraHeaders"], [("K1", "V1"), ("K2", "V2")])
def test_add_header_resource(self) -> None:
"""
When --add-header is specified, the resource is a composition that adds
headers.
"""
options = Options()
options.parseOptions(["--add-header", "K1: V1", "--add-header", "K2: V2"])
service = makeService(options)
resource = service.services[0].factory.resource
self.assertIsInstance(resource, _AddHeadersResource)
self.assertEqual(resource._headers, [("K1", "V1"), ("K2", "V2")])
self.assertIsInstance(resource._originalResource, demo.Test)
def test_noTracebacksDeprecation(self) -> None:
"""
Passing --notracebacks is deprecated.
"""
options = Options()
options.parseOptions(["--notracebacks"])
makeService(options)
warnings = self.flushWarnings([self.test_noTracebacksDeprecation])
self.assertEqual(warnings[0]["category"], DeprecationWarning)
self.assertEqual(
warnings[0]["message"], "--notracebacks was deprecated in Twisted 19.7.0"
)
self.assertEqual(len(warnings), 1)
def test_displayTracebacks(self) -> None:
"""
Passing --display-tracebacks will enable traceback rendering on the
generated Site.
"""
options = Options()
options.parseOptions(["--display-tracebacks"])
service = makeService(options)
self.assertTrue(service.services[0].factory.displayTracebacks)
def test_displayTracebacksNotGiven(self) -> None:
"""
Not passing --display-tracebacks will leave traceback rendering on the
generated Site off.
"""
options = Options()
options.parseOptions([])
service = makeService(options)
self.assertFalse(service.services[0].factory.displayTracebacks)
class AddHeadersResourceTests(TestCase):
def test_getChildWithDefault(self) -> None:
"""
When getChildWithDefault is invoked, it adds the headers to the
response.
"""
resource = _AddHeadersResource(
demo.Test(), [("K1", "V1"), ("K2", "V2"), ("K1", "V3")]
)
request = DummyRequest([])
resource.getChildWithDefault("", request)
self.assertEqual(request.responseHeaders.getRawHeaders("K1"), ["V1", "V3"])
self.assertEqual(request.responseHeaders.getRawHeaders("K2"), ["V2"])

View File

@@ -0,0 +1,915 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.template}
"""
import sys
from io import StringIO
from typing import List, Optional
from zope.interface import implementer
from zope.interface.verify import verifyObject
from twisted.internet.defer import Deferred, succeed
from twisted.internet.testing import EventLoggingObserver
from twisted.logger import globalLogPublisher
from twisted.python.failure import Failure
from twisted.python.filepath import FilePath
from twisted.trial.unittest import TestCase
from twisted.trial.util import suppress as SUPPRESS
from twisted.web._element import UnexposedMethodError
from twisted.web.error import FlattenerError, MissingRenderMethod, MissingTemplateLoader
from twisted.web.iweb import IRequest, ITemplateLoader
from twisted.web.server import NOT_DONE_YET
from twisted.web.template import (
Element,
Flattenable,
Tag,
TagLoader,
XMLFile,
XMLString,
renderElement,
renderer,
tags,
)
from twisted.web.test._util import FlattenTestCase
from twisted.web.test.test_web import DummyRequest
_xmlFileSuppress = SUPPRESS(
category=DeprecationWarning,
message="Passing filenames or file objects to XMLFile is "
"deprecated since Twisted 12.1. Pass a FilePath instead.",
)
class TagFactoryTests(TestCase):
"""
Tests for L{_TagFactory} through the publicly-exposed L{tags} object.
"""
def test_lookupTag(self) -> None:
"""
HTML tags can be retrieved through C{tags}.
"""
tag = tags.a
self.assertEqual(tag.tagName, "a")
def test_lookupHTML5Tag(self) -> None:
"""
Twisted supports the latest and greatest HTML tags from the HTML5
specification.
"""
tag = tags.video
self.assertEqual(tag.tagName, "video")
def test_lookupTransparentTag(self) -> None:
"""
To support transparent inclusion in templates, there is a special tag,
the transparent tag, which has no name of its own but is accessed
through the "transparent" attribute.
"""
tag = tags.transparent
self.assertEqual(tag.tagName, "")
def test_lookupInvalidTag(self) -> None:
"""
Invalid tags which are not part of HTML cause AttributeErrors when
accessed through C{tags}.
"""
self.assertRaises(AttributeError, getattr, tags, "invalid")
def test_lookupXMP(self) -> None:
"""
As a special case, the <xmp> tag is simply not available through
C{tags} or any other part of the templating machinery.
"""
self.assertRaises(AttributeError, getattr, tags, "xmp")
class ElementTests(TestCase):
"""
Tests for the awesome new L{Element} class.
"""
def test_missingTemplateLoader(self) -> None:
"""
L{Element.render} raises L{MissingTemplateLoader} if the C{loader}
attribute is L{None}.
"""
element = Element()
err = self.assertRaises(MissingTemplateLoader, element.render, None)
self.assertIdentical(err.element, element)
def test_missingTemplateLoaderRepr(self) -> None:
"""
A L{MissingTemplateLoader} instance can be repr()'d without error.
"""
class PrettyReprElement(Element):
def __repr__(self) -> str:
return "Pretty Repr Element"
self.assertIn(
"Pretty Repr Element", repr(MissingTemplateLoader(PrettyReprElement()))
)
def test_missingRendererMethod(self) -> None:
"""
When called with the name which is not associated with a render method,
L{Element.lookupRenderMethod} raises L{MissingRenderMethod}.
"""
element = Element()
err = self.assertRaises(MissingRenderMethod, element.lookupRenderMethod, "foo")
self.assertIdentical(err.element, element)
self.assertEqual(err.renderName, "foo")
def test_missingRenderMethodRepr(self) -> None:
"""
A L{MissingRenderMethod} instance can be repr()'d without error.
"""
class PrettyReprElement(Element):
def __repr__(self) -> str:
return "Pretty Repr Element"
s = repr(MissingRenderMethod(PrettyReprElement(), "expectedMethod"))
self.assertIn("Pretty Repr Element", s)
self.assertIn("expectedMethod", s)
def test_definedRenderer(self) -> None:
"""
When called with the name of a defined render method,
L{Element.lookupRenderMethod} returns that render method.
"""
class ElementWithRenderMethod(Element):
@renderer
def foo(self, request: Optional[IRequest], tag: Tag) -> Flattenable:
return "bar"
foo = ElementWithRenderMethod().lookupRenderMethod("foo")
self.assertEqual(foo(None, tags.br), "bar")
def test_render(self) -> None:
"""
L{Element.render} loads a document from the C{loader} attribute and
returns it.
"""
@implementer(ITemplateLoader)
class TemplateLoader:
def load(self) -> List[Flattenable]:
return ["result"]
class StubElement(Element):
loader = TemplateLoader()
element = StubElement()
self.assertEqual(element.render(None), ["result"])
def test_misuseRenderer(self) -> None:
"""
If the L{renderer} decorator is called without any arguments, it will
raise a comprehensible exception.
"""
te = self.assertRaises(TypeError, renderer)
if sys.version_info >= (3, 10):
self.assertEqual(
str(te), "Expose.__call__() missing 1 required positional argument: 'f'"
)
else:
self.assertEqual(
str(te), "__call__() missing 1 required positional argument: 'f'"
)
def test_renderGetDirectlyError(self) -> None:
"""
Called directly, without a default, L{renderer.get} raises
L{UnexposedMethodError} when it cannot find a renderer.
"""
self.assertRaises(UnexposedMethodError, renderer.get, None, "notARenderer")
class XMLFileReprTests(TestCase):
"""
Tests for L{twisted.web.template.XMLFile}'s C{__repr__}.
"""
def test_filePath(self) -> None:
"""
An L{XMLFile} with a L{FilePath} returns a useful repr().
"""
path = FilePath("/tmp/fake.xml")
self.assertEqual(f"<XMLFile of {path!r}>", repr(XMLFile(path)))
def test_filename(self) -> None:
"""
An L{XMLFile} with a filename returns a useful repr().
"""
fname = "/tmp/fake.xml" # deprecated
self.assertEqual(f"<XMLFile of {fname!r}>", repr(XMLFile(fname))) # type: ignore[arg-type]
test_filename.suppress = [_xmlFileSuppress] # type: ignore[attr-defined]
def test_file(self) -> None:
"""
An L{XMLFile} with a file object returns a useful repr().
"""
fobj = StringIO("not xml") # deprecated
self.assertEqual(f"<XMLFile of {fobj!r}>", repr(XMLFile(fobj))) # type: ignore[arg-type]
test_file.suppress = [_xmlFileSuppress] # type: ignore[attr-defined]
class XMLLoaderTestsMixin:
deprecatedUse: bool
"""
C{True} if this use of L{XMLFile} is deprecated and should emit
a C{DeprecationWarning}.
"""
templateString = "<p>Hello, world.</p>"
"""
Simple template to use to exercise the loaders.
"""
def loaderFactory(self) -> ITemplateLoader:
raise NotImplementedError
def test_load(self) -> None:
"""
Verify that the loader returns a tag with the correct children.
"""
assert isinstance(self, TestCase)
loader = self.loaderFactory()
(tag,) = loader.load()
assert isinstance(tag, Tag)
warnings = self.flushWarnings(offendingFunctions=[self.loaderFactory])
if self.deprecatedUse:
self.assertEqual(len(warnings), 1)
self.assertEqual(warnings[0]["category"], DeprecationWarning)
self.assertEqual(
warnings[0]["message"],
"Passing filenames or file objects to XMLFile is "
"deprecated since Twisted 12.1. Pass a FilePath instead.",
)
else:
self.assertEqual(len(warnings), 0)
self.assertEqual(tag.tagName, "p")
self.assertEqual(tag.children, ["Hello, world."])
def test_loadTwice(self) -> None:
"""
If {load()} can be called on a loader twice the result should be the
same.
"""
assert isinstance(self, TestCase)
loader = self.loaderFactory()
tags1 = loader.load()
tags2 = loader.load()
self.assertEqual(tags1, tags2)
test_loadTwice.suppress = [_xmlFileSuppress] # type: ignore[attr-defined]
class XMLStringLoaderTests(TestCase, XMLLoaderTestsMixin):
"""
Tests for L{twisted.web.template.XMLString}
"""
deprecatedUse = False
def loaderFactory(self) -> ITemplateLoader:
"""
@return: an L{XMLString} constructed with C{self.templateString}.
"""
return XMLString(self.templateString)
class XMLFileWithFilePathTests(TestCase, XMLLoaderTestsMixin):
"""
Tests for L{twisted.web.template.XMLFile}'s L{FilePath} support.
"""
deprecatedUse = False
def loaderFactory(self) -> ITemplateLoader:
"""
@return: an L{XMLString} constructed with a L{FilePath} pointing to a
file that contains C{self.templateString}.
"""
fp = FilePath(self.mktemp())
fp.setContent(self.templateString.encode("utf8"))
return XMLFile(fp)
class XMLFileWithFileTests(TestCase, XMLLoaderTestsMixin):
"""
Tests for L{twisted.web.template.XMLFile}'s deprecated file object support.
"""
deprecatedUse = True
def loaderFactory(self) -> ITemplateLoader:
"""
@return: an L{XMLString} constructed with a file object that contains
C{self.templateString}.
"""
return XMLFile(StringIO(self.templateString)) # type: ignore[arg-type]
class XMLFileWithFilenameTests(TestCase, XMLLoaderTestsMixin):
"""
Tests for L{twisted.web.template.XMLFile}'s deprecated filename support.
"""
deprecatedUse = True
def loaderFactory(self) -> ITemplateLoader:
"""
@return: an L{XMLString} constructed with a filename that points to a
file containing C{self.templateString}.
"""
fp = FilePath(self.mktemp())
fp.setContent(self.templateString.encode("utf8"))
return XMLFile(fp.path)
class FlattenIntegrationTests(FlattenTestCase):
"""
Tests for integration between L{Element} and
L{twisted.web._flatten.flatten}.
"""
def test_roundTrip(self) -> None:
"""
Given a series of parsable XML strings, verify that
L{twisted.web._flatten.flatten} will flatten the L{Element} back to the
input when sent on a round trip.
"""
fragments = [
b"<p>Hello, world.</p>",
b"<p><!-- hello, world --></p>",
b"<p><![CDATA[Hello, world.]]></p>",
b'<test1 xmlns:test2="urn:test2">' b"<test2:test3></test2:test3></test1>",
b'<test1 xmlns="urn:test2"><test3></test3></test1>',
b"<p>\xe2\x98\x83</p>",
]
for xml in fragments:
self.assertFlattensImmediately(Element(loader=XMLString(xml)), xml)
def test_entityConversion(self) -> None:
"""
When flattening an HTML entity, it should flatten out to the utf-8
representation if possible.
"""
element = Element(loader=XMLString("<p>&#9731;</p>"))
self.assertFlattensImmediately(element, b"<p>\xe2\x98\x83</p>")
def test_missingTemplateLoader(self) -> None:
"""
Rendering an Element without a loader attribute raises the appropriate
exception.
"""
self.assertFlatteningRaises(Element(), MissingTemplateLoader)
def test_missingRenderMethod(self) -> None:
"""
Flattening an L{Element} with a C{loader} which has a tag with a render
directive fails with L{FlattenerError} if there is no available render
method to satisfy that directive.
"""
element = Element(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="unknownMethod" />
"""
)
)
self.assertFlatteningRaises(element, MissingRenderMethod)
def test_transparentRendering(self) -> None:
"""
A C{transparent} element should be eliminated from the DOM and rendered as
only its children.
"""
element = Element(
loader=XMLString(
"<t:transparent "
'xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
"Hello, world."
"</t:transparent>"
)
)
self.assertFlattensImmediately(element, b"Hello, world.")
def test_attrRendering(self) -> None:
"""
An Element with an attr tag renders the vaule of its attr tag as an
attribute of its containing tag.
"""
element = Element(
loader=XMLString(
'<a xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
'<t:attr name="href">http://example.com</t:attr>'
"Hello, world."
"</a>"
)
)
self.assertFlattensImmediately(
element, b'<a href="http://example.com">Hello, world.</a>'
)
def test_synchronousDeferredRecursion(self) -> None:
"""
When rendering a large number of already-fired Deferreds we should not
encounter any recursion errors or stack-depth issues.
"""
self.assertFlattensImmediately([succeed("x") for i in range(250)], b"x" * 250)
def test_errorToplevelAttr(self) -> None:
"""
A template with a toplevel C{attr} tag will not load; it will raise
L{AssertionError} if you try.
"""
self.assertRaises(
AssertionError,
XMLString,
"""<t:attr
xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
name='something'
>hello</t:attr>
""",
)
def test_errorUnnamedAttr(self) -> None:
"""
A template with an C{attr} tag with no C{name} attribute will not load;
it will raise L{AssertionError} if you try.
"""
self.assertRaises(
AssertionError,
XMLString,
"""<html><t:attr
xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
>hello</t:attr></html>""",
)
def test_lenientPrefixBehavior(self) -> None:
"""
If the parser sees a prefix it doesn't recognize on an attribute, it
will pass it on through to serialization.
"""
theInput = (
'<hello:world hello:sample="testing" '
'xmlns:hello="http://made-up.example.com/ns/not-real">'
"This is a made-up tag.</hello:world>"
)
element = Element(loader=XMLString(theInput))
self.assertFlattensTo(element, theInput.encode("utf8"))
def test_deferredRendering(self) -> None:
"""
An Element with a render method which returns a Deferred will render
correctly.
"""
class RenderfulElement(Element):
@renderer
def renderMethod(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
return succeed("Hello, world.")
element = RenderfulElement(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="renderMethod">
Goodbye, world.
</p>
"""
)
)
self.assertFlattensImmediately(element, b"Hello, world.")
def test_loaderClassAttribute(self) -> None:
"""
If there is a non-None loader attribute on the class of an Element
instance but none on the instance itself, the class attribute is used.
"""
class SubElement(Element):
loader = XMLString("<p>Hello, world.</p>")
self.assertFlattensImmediately(SubElement(), b"<p>Hello, world.</p>")
def test_directiveRendering(self) -> None:
"""
An Element with a valid render directive has that directive invoked and
the result added to the output.
"""
renders = []
class RenderfulElement(Element):
@renderer
def renderMethod(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
renders.append((self, request))
return tag("Hello, world.")
element = RenderfulElement(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="renderMethod" />
"""
)
)
self.assertFlattensImmediately(element, b"<p>Hello, world.</p>")
def test_directiveRenderingOmittingTag(self) -> None:
"""
An Element with a render method which omits the containing tag
successfully removes that tag from the output.
"""
class RenderfulElement(Element):
@renderer
def renderMethod(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
return "Hello, world."
element = RenderfulElement(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="renderMethod">
Goodbye, world.
</p>
"""
)
)
self.assertFlattensImmediately(element, b"Hello, world.")
def test_elementContainingStaticElement(self) -> None:
"""
An Element which is returned by the render method of another Element is
rendered properly.
"""
class RenderfulElement(Element):
@renderer
def renderMethod(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
return tag(Element(loader=XMLString("<em>Hello, world.</em>")))
element = RenderfulElement(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="renderMethod" />
"""
)
)
self.assertFlattensImmediately(element, b"<p><em>Hello, world.</em></p>")
def test_elementUsingSlots(self) -> None:
"""
An Element which is returned by the render method of another Element is
rendered properly.
"""
class RenderfulElement(Element):
@renderer
def renderMethod(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
return tag.fillSlots(test2="world.")
element = RenderfulElement(
loader=XMLString(
'<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"'
' t:render="renderMethod">'
'<t:slot name="test1" default="Hello, " />'
'<t:slot name="test2" />'
"</p>"
)
)
self.assertFlattensImmediately(element, b"<p>Hello, world.</p>")
def test_elementContainingDynamicElement(self) -> None:
"""
Directives in the document factory of an Element returned from a render
method of another Element are satisfied from the correct object: the
"inner" Element.
"""
class OuterElement(Element):
@renderer
def outerMethod(self, request: Optional[IRequest], tag: Tag) -> Flattenable:
return tag(
InnerElement(
loader=XMLString(
"""
<t:ignored
xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="innerMethod" />
"""
)
)
)
class InnerElement(Element):
@renderer
def innerMethod(self, request: Optional[IRequest], tag: Tag) -> Flattenable:
return "Hello, world."
element = OuterElement(
loader=XMLString(
"""
<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="outerMethod" />
"""
)
)
self.assertFlattensImmediately(element, b"<p>Hello, world.</p>")
def test_sameLoaderTwice(self) -> None:
"""
Rendering the output of a loader, or even the same element, should
return different output each time.
"""
sharedLoader = XMLString(
'<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
'<t:transparent t:render="classCounter" /> '
'<t:transparent t:render="instanceCounter" />'
"</p>"
)
class DestructiveElement(Element):
count = 0
instanceCount = 0
loader = sharedLoader
@renderer
def classCounter(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
DestructiveElement.count += 1
return tag(str(DestructiveElement.count))
@renderer
def instanceCounter(
self, request: Optional[IRequest], tag: Tag
) -> Flattenable:
self.instanceCount += 1
return tag(str(self.instanceCount))
e1 = DestructiveElement()
e2 = DestructiveElement()
self.assertFlattensImmediately(e1, b"<p>1 1</p>")
self.assertFlattensImmediately(e1, b"<p>2 2</p>")
self.assertFlattensImmediately(e2, b"<p>3 1</p>")
class TagLoaderTests(FlattenTestCase):
"""
Tests for L{TagLoader}.
"""
def setUp(self) -> None:
self.loader = TagLoader(tags.i("test"))
def test_interface(self) -> None:
"""
An instance of L{TagLoader} provides L{ITemplateLoader}.
"""
self.assertTrue(verifyObject(ITemplateLoader, self.loader))
def test_loadsList(self) -> None:
"""
L{TagLoader.load} returns a list, per L{ITemplateLoader}.
"""
self.assertIsInstance(self.loader.load(), list)
def test_flatten(self) -> None:
"""
L{TagLoader} can be used in an L{Element}, and flattens as the tag used
to construct the L{TagLoader} would flatten.
"""
e = Element(self.loader)
self.assertFlattensImmediately(e, b"<i>test</i>")
class TestElement(Element):
"""
An L{Element} that can be rendered successfully.
"""
loader = XMLString(
'<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
"Hello, world."
"</p>"
)
class TestFailureElement(Element):
"""
An L{Element} that can be used in place of L{FailureElement} to verify
that L{renderElement} can render failures properly.
"""
loader = XMLString(
'<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
"I failed."
"</p>"
)
def __init__(self, failure: Failure, loader: object = None) -> None:
self.failure = failure
class FailingElement(Element):
"""
An element that raises an exception when rendered.
"""
def render(self, request: Optional[IRequest]) -> "Flattenable":
a = 42
b = 0
return f"{a // b}"
class FakeSite:
"""
A minimal L{Site} object that we can use to test displayTracebacks
"""
displayTracebacks = False
@implementer(IRequest)
class DummyRenderRequest(DummyRequest): # type: ignore[misc]
"""
A dummy request object that has a C{site} attribute.
This does not implement the full IRequest interface, but enough of it
for this test suite.
"""
def __init__(self) -> None:
super().__init__([b""])
self.site = FakeSite()
class RenderElementTests(TestCase):
"""
Test L{renderElement}
"""
def setUp(self) -> None:
"""
Set up a common L{DummyRenderRequest}.
"""
self.request = DummyRenderRequest()
def test_simpleRender(self) -> Deferred[None]:
"""
L{renderElement} returns NOT_DONE_YET and eventually
writes the rendered L{Element} to the request before finishing the
request.
"""
element = TestElement()
d = self.request.notifyFinish()
def check(_: object) -> None:
self.assertEqual(
b"".join(self.request.written),
b"<!DOCTYPE html>\n" b"<p>Hello, world.</p>",
)
self.assertTrue(self.request.finished)
d.addCallback(check)
self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
return d
def test_simpleFailure(self) -> Deferred[None]:
"""
L{renderElement} handles failures by writing a minimal
error message to the request and finishing it.
"""
element = FailingElement()
d = self.request.notifyFinish()
def check(_: object) -> None:
flushed = self.flushLoggedErrors(FlattenerError)
self.assertEqual(len(flushed), 1)
self.assertEqual(
b"".join(self.request.written),
(
b"<!DOCTYPE html>\n"
b'<div style="font-size:800%;'
b"background-color:#FFF;"
b"color:#F00"
b'">An error occurred while rendering the response.</div>'
),
)
self.assertTrue(self.request.finished)
d.addCallback(check)
self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
return d
def test_simpleFailureWithTraceback(self) -> Deferred[None]:
"""
L{renderElement} will render a traceback when rendering of
the element fails and our site is configured to display tracebacks.
"""
logObserver = EventLoggingObserver.createWithCleanup(self, globalLogPublisher)
self.request.site.displayTracebacks = True
element = FailingElement()
d = self.request.notifyFinish()
def check(_: object) -> None:
self.assertEquals(1, len(logObserver))
f = logObserver[0]["log_failure"]
self.assertIsInstance(f.value, FlattenerError)
flushed = self.flushLoggedErrors(FlattenerError)
self.assertEqual(len(flushed), 1)
self.assertEqual(
b"".join(self.request.written), b"<!DOCTYPE html>\n<p>I failed.</p>"
)
self.assertTrue(self.request.finished)
d.addCallback(check)
renderElement(self.request, element, _failElement=TestFailureElement)
return d
def test_nonDefaultDoctype(self) -> Deferred[None]:
"""
L{renderElement} will write the doctype string specified by the
doctype keyword argument.
"""
element = TestElement()
d = self.request.notifyFinish()
def check(_: object) -> None:
self.assertEqual(
b"".join(self.request.written),
(
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
b' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
b"<p>Hello, world.</p>"
),
)
d.addCallback(check)
renderElement(
self.request,
element,
doctype=(
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
b' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
),
)
return d
def test_noneDoctype(self) -> Deferred[None]:
"""
L{renderElement} will not write out a doctype if the doctype keyword
argument is L{None}.
"""
element = TestElement()
d = self.request.notifyFinish()
def check(_: object) -> None:
self.assertEqual(b"".join(self.request.written), b"<p>Hello, world.</p>")
d.addCallback(check)
renderElement(self.request, element, doctype=None)
return d

View File

@@ -0,0 +1,433 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.util}.
"""
import gc
from twisted.internet import defer
from twisted.python.compat import networkString
from twisted.python.failure import Failure
from twisted.trial.unittest import SynchronousTestCase, TestCase
from twisted.web import resource, util
from twisted.web.error import FlattenerError
from twisted.web.http import FOUND
from twisted.web.server import Request
from twisted.web.template import TagLoader, flattenString, tags
from twisted.web.test.requesthelper import DummyChannel, DummyRequest
from twisted.web.util import (
DeferredResource,
FailureElement,
ParentRedirect,
_FrameElement,
_SourceFragmentElement,
_SourceLineElement,
_StackElement,
formatFailure,
redirectTo,
)
class RedirectToTests(TestCase):
"""
Tests for L{redirectTo}.
"""
def test_headersAndCode(self):
"""
L{redirectTo} will set the C{Location} and C{Content-Type} headers on
its request, and set the response code to C{FOUND}, so the browser will
be redirected.
"""
request = Request(DummyChannel(), True)
request.method = b"GET"
targetURL = b"http://target.example.com/4321"
redirectTo(targetURL, request)
self.assertEqual(request.code, FOUND)
self.assertEqual(
request.responseHeaders.getRawHeaders(b"location"), [targetURL]
)
self.assertEqual(
request.responseHeaders.getRawHeaders(b"content-type"),
[b"text/html; charset=utf-8"],
)
def test_redirectToUnicodeURL(self):
"""
L{redirectTo} will raise TypeError if unicode object is passed in URL
"""
request = Request(DummyChannel(), True)
request.method = b"GET"
targetURL = "http://target.example.com/4321"
self.assertRaises(TypeError, redirectTo, targetURL, request)
def test_legitimateRedirect(self):
"""
Legitimate URLs are fully interpolated in the `redirectTo` response body without transformation
"""
request = DummyRequest([b""])
html = redirectTo(b"https://twisted.org/", request)
expected = b"""
<html>
<head>
<meta http-equiv=\"refresh\" content=\"0;URL=https://twisted.org/\">
</head>
<body bgcolor=\"#FFFFFF\" text=\"#000000\">
<a href=\"https://twisted.org/\">click here</a>
</body>
</html>
"""
self.assertEqual(html, expected)
def test_maliciousRedirect(self):
"""
Malicious URLs are HTML-escaped before interpolating them in the `redirectTo` response body
"""
request = DummyRequest([b""])
html = redirectTo(
b'https://twisted.org/"><script>alert(document.location)</script>', request
)
expected = b"""
<html>
<head>
<meta http-equiv=\"refresh\" content=\"0;URL=https://twisted.org/&quot;&gt;&lt;script&gt;alert(document.location)&lt;/script&gt;\">
</head>
<body bgcolor=\"#FFFFFF\" text=\"#000000\">
<a href=\"https://twisted.org/&quot;&gt;&lt;script&gt;alert(document.location)&lt;/script&gt;\">click here</a>
</body>
</html>
"""
self.assertEqual(html, expected)
class ParentRedirectTests(SynchronousTestCase):
"""
Test L{ParentRedirect}.
"""
def doLocationTest(self, requestPath: bytes) -> bytes:
"""
Render a response to a request with path *requestPath*
@param requestPath: A slash-separated path like C{b'/foo/bar'}.
@returns: The value of the I{Location} header.
"""
request = Request(DummyChannel(), True)
request.method = b"GET"
request.prepath = requestPath.lstrip(b"/").split(b"/")
resource = ParentRedirect()
resource.render(request)
headers = request.responseHeaders.getRawHeaders(b"Location")
assert headers is not None
[location] = headers
return location
def test_locationRoot(self):
"""
At the URL root issue a redirect to the current URL, removing any query
string.
"""
self.assertEqual(b"http://10.0.0.1/", self.doLocationTest(b"/"))
self.assertEqual(b"http://10.0.0.1/", self.doLocationTest(b"/?biff=baff"))
def test_locationToRoot(self):
"""
A request for a resource one level down from the URL root produces
a redirect to the root.
"""
self.assertEqual(b"http://10.0.0.1/", self.doLocationTest(b"/foo"))
self.assertEqual(
b"http://10.0.0.1/", self.doLocationTest(b"/foo?bar=sproiiing")
)
def test_locationUpOne(self):
"""
Requests for resources directly under the path C{/foo/} produce
redirects to C{/foo/}.
"""
self.assertEqual(b"http://10.0.0.1/foo/", self.doLocationTest(b"/foo/"))
self.assertEqual(b"http://10.0.0.1/foo/", self.doLocationTest(b"/foo/bar"))
self.assertEqual(
b"http://10.0.0.1/foo/", self.doLocationTest(b"/foo/bar?biz=baz")
)
class FailureElementTests(TestCase):
"""
Tests for L{FailureElement} and related helpers which can render a
L{Failure} as an HTML string.
"""
def setUp(self):
"""
Create a L{Failure} which can be used by the rendering tests.
"""
def lineNumberProbeAlsoBroken():
message = "This is a problem"
raise Exception(message)
# Figure out the line number from which the exception will be raised.
self.base = lineNumberProbeAlsoBroken.__code__.co_firstlineno + 1
try:
lineNumberProbeAlsoBroken()
except BaseException:
self.failure = Failure(captureVars=True)
self.frame = self.failure.frames[-1]
def test_sourceLineElement(self):
"""
L{_SourceLineElement} renders a source line and line number.
"""
element = _SourceLineElement(
TagLoader(
tags.div(tags.span(render="lineNumber"), tags.span(render="sourceLine"))
),
50,
" print 'hello'",
)
d = flattenString(None, element)
expected = (
"<div><span>50</span><span>"
" \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}print 'hello'</span></div>"
)
d.addCallback(self.assertEqual, expected.encode("utf-8"))
return d
def test_sourceFragmentElement(self):
"""
L{_SourceFragmentElement} renders source lines at and around the line
number indicated by a frame object.
"""
element = _SourceFragmentElement(
TagLoader(
tags.div(
tags.span(render="lineNumber"),
tags.span(render="sourceLine"),
render="sourceLines",
)
),
self.frame,
)
source = [
" \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}message = " '"This is a problem"',
" \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}raise Exception(message)",
"",
]
d = flattenString(None, element)
stringToCheckFor = ""
for lineNumber, sourceLine in enumerate(source):
template = '<div class="snippet{}Line"><span>{}</span><span>{}</span></div>'
if lineNumber <= 1:
stringToCheckFor += template.format(
["", "Highlight"][lineNumber == 1],
self.base + lineNumber,
(" \N{NO-BREAK SPACE}" * 4 + sourceLine),
)
else:
stringToCheckFor += template.format(
"", self.base + lineNumber, ("" + sourceLine)
)
bytesToCheckFor = stringToCheckFor.encode("utf8")
d.addCallback(self.assertEqual, bytesToCheckFor)
return d
def test_frameElementFilename(self):
"""
The I{filename} renderer of L{_FrameElement} renders the filename
associated with the frame object used to initialize the
L{_FrameElement}.
"""
element = _FrameElement(TagLoader(tags.span(render="filename")), self.frame)
d = flattenString(None, element)
d.addCallback(
# __file__ differs depending on whether an up-to-date .pyc file
# already existed.
self.assertEqual,
b"<span>" + networkString(__file__.rstrip("c")) + b"</span>",
)
return d
def test_frameElementLineNumber(self):
"""
The I{lineNumber} renderer of L{_FrameElement} renders the line number
associated with the frame object used to initialize the
L{_FrameElement}.
"""
element = _FrameElement(TagLoader(tags.span(render="lineNumber")), self.frame)
d = flattenString(None, element)
d.addCallback(self.assertEqual, b"<span>%d</span>" % (self.base + 1,))
return d
def test_frameElementFunction(self):
"""
The I{function} renderer of L{_FrameElement} renders the line number
associated with the frame object used to initialize the
L{_FrameElement}.
"""
element = _FrameElement(TagLoader(tags.span(render="function")), self.frame)
d = flattenString(None, element)
d.addCallback(self.assertEqual, b"<span>lineNumberProbeAlsoBroken</span>")
return d
def test_frameElementSource(self):
"""
The I{source} renderer of L{_FrameElement} renders the source code near
the source filename/line number associated with the frame object used to
initialize the L{_FrameElement}.
"""
element = _FrameElement(None, self.frame)
renderer = element.lookupRenderMethod("source")
tag = tags.div()
result = renderer(None, tag)
self.assertIsInstance(result, _SourceFragmentElement)
self.assertIdentical(result.frame, self.frame)
self.assertEqual([tag], result.loader.load())
def test_stackElement(self):
"""
The I{frames} renderer of L{_StackElement} renders each stack frame in
the list of frames used to initialize the L{_StackElement}.
"""
element = _StackElement(None, self.failure.frames[:2])
renderer = element.lookupRenderMethod("frames")
tag = tags.div()
result = renderer(None, tag)
self.assertIsInstance(result, list)
self.assertIsInstance(result[0], _FrameElement)
self.assertIdentical(result[0].frame, self.failure.frames[0])
self.assertIsInstance(result[1], _FrameElement)
self.assertIdentical(result[1].frame, self.failure.frames[1])
# They must not share the same tag object.
self.assertNotEqual(result[0].loader.load(), result[1].loader.load())
self.assertEqual(2, len(result))
def test_failureElementTraceback(self):
"""
The I{traceback} renderer of L{FailureElement} renders the failure's
stack frames using L{_StackElement}.
"""
element = FailureElement(self.failure)
renderer = element.lookupRenderMethod("traceback")
tag = tags.div()
result = renderer(None, tag)
self.assertIsInstance(result, _StackElement)
self.assertIdentical(result.stackFrames, self.failure.frames)
self.assertEqual([tag], result.loader.load())
def test_failureElementType(self):
"""
The I{type} renderer of L{FailureElement} renders the failure's
exception type.
"""
element = FailureElement(self.failure, TagLoader(tags.span(render="type")))
d = flattenString(None, element)
exc = b"builtins.Exception"
d.addCallback(self.assertEqual, b"<span>" + exc + b"</span>")
return d
def test_failureElementValue(self):
"""
The I{value} renderer of L{FailureElement} renders the value's exception
value.
"""
element = FailureElement(self.failure, TagLoader(tags.span(render="value")))
d = flattenString(None, element)
d.addCallback(self.assertEqual, b"<span>This is a problem</span>")
return d
class FormatFailureTests(TestCase):
"""
Tests for L{twisted.web.util.formatFailure} which returns an HTML string
representing the L{Failure} instance passed to it.
"""
def test_flattenerError(self):
"""
If there is an error flattening the L{Failure} instance,
L{formatFailure} raises L{FlattenerError}.
"""
self.assertRaises(FlattenerError, formatFailure, object())
def test_returnsBytes(self):
"""
The return value of L{formatFailure} is a C{str} instance (not a
C{unicode} instance) with numeric character references for any non-ASCII
characters meant to appear in the output.
"""
try:
raise Exception("Fake bug")
except BaseException:
result = formatFailure(Failure())
self.assertIsInstance(result, bytes)
self.assertTrue(all(ch < 128 for ch in result))
# Indentation happens to rely on NO-BREAK SPACE
self.assertIn(b"&#160;", result)
class SDResource(resource.Resource):
def __init__(self, default):
self.default = default
def getChildWithDefault(self, name, request):
d = defer.succeed(self.default)
resource = util.DeferredResource(d)
return resource.getChildWithDefault(name, request)
class DeferredResourceTests(SynchronousTestCase):
"""
Tests for L{DeferredResource}.
"""
def testDeferredResource(self):
r = resource.Resource()
r.isLeaf = 1
s = SDResource(r)
d = DummyRequest(["foo", "bar", "baz"])
resource.getChildForRequest(s, d)
self.assertEqual(d.postpath, ["bar", "baz"])
def test_render(self):
"""
L{DeferredResource} uses the request object's C{render} method to
render the resource which is the result of the L{Deferred} being
handled.
"""
rendered = []
request = DummyRequest([])
request.render = rendered.append
result = resource.Resource()
deferredResource = DeferredResource(defer.succeed(result))
deferredResource.render(request)
self.assertEqual(rendered, [result])
def test_renderNoFailure(self):
"""
If the L{Deferred} fails, L{DeferredResource} reports the failure via
C{processingFailed}, and does not cause an unhandled error to be
logged.
"""
request = DummyRequest([])
d = request.notifyFinish()
failure = Failure(RuntimeError())
deferredResource = DeferredResource(defer.fail(failure))
deferredResource.render(request)
self.assertEqual(self.failureResultOf(d), failure)
del deferredResource
gc.collect()
errors = self.flushLoggedErrors(RuntimeError)
self.assertEqual(errors, [])

View File

@@ -0,0 +1,213 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web.vhost}.
"""
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.server import Site
from twisted.web.static import Data
from twisted.web.test._util import _render
from twisted.web.test.test_web import DummyRequest
from twisted.web.vhost import NameVirtualHost, VHostMonsterResource, _HostResource
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([b""])
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([b""])
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([b""])
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
async def test_renderWithHTMLHost(self):
"""
L{NameVirtualHost.render} doesn't echo unescaped HTML when present in
the I{Host} header.
"""
virtualHostResource = NameVirtualHost()
request = DummyRequest([b""])
request.requestHeaders.addRawHeader(b"host", b"<b>example</b>.com")
await _render(virtualHostResource, request)
self.assertNotIn(b"<b>", b"".join(request.written))
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

View File

@@ -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.
"""
import string
from twisted.trial import unittest
from twisted.web import _responses
class ResponseTests(unittest.TestCase):
def test_constants(self) -> None:
"""
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)

View File

@@ -0,0 +1,367 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests L{twisted.web.client} helper APIs
"""
from urllib.parse import urlparse
from twisted.trial import unittest
from twisted.web import client
class URLJoinTests(unittest.TestCase):
"""
Tests for L{client._urljoin}.
"""
def test_noFragments(self):
"""
L{client._urljoin} does not include a fragment identifier in the
resulting URL if neither the base nor the new path include a fragment
identifier.
"""
self.assertEqual(
client._urljoin(b"http://foo.com/bar", b"/quux"), b"http://foo.com/quux"
)
self.assertEqual(
client._urljoin(b"http://foo.com/bar#", b"/quux"), b"http://foo.com/quux"
)
self.assertEqual(
client._urljoin(b"http://foo.com/bar", b"/quux#"), b"http://foo.com/quux"
)
def test_preserveFragments(self):
"""
L{client._urljoin} preserves the fragment identifier from either the
new path or the base URL respectively, as specified in the HTTP 1.1 bis
draft.
@see: U{https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-22#section-7.1.2}
"""
self.assertEqual(
client._urljoin(b"http://foo.com/bar#frag", b"/quux"),
b"http://foo.com/quux#frag",
)
self.assertEqual(
client._urljoin(b"http://foo.com/bar", b"/quux#frag2"),
b"http://foo.com/quux#frag2",
)
self.assertEqual(
client._urljoin(b"http://foo.com/bar#frag", b"/quux#frag2"),
b"http://foo.com/quux#frag2",
)
class URITests:
"""
Abstract tests for L{twisted.web.client.URI}.
Subclass this and L{unittest.TestCase}. Then provide a value for
C{host} and C{uriHost}.
@ivar host: A host specification for use in tests, must be L{bytes}.
@ivar uriHost: The host specification in URI form, must be a L{bytes}. In
most cases this is identical with C{host}. IPv6 address literals are an
exception, according to RFC 3986 section 3.2.2, as they need to be
enclosed in brackets. In this case this variable is different.
"""
def makeURIString(self, template):
"""
Replace the string "HOST" in C{template} with this test's host.
Byte strings Python between (and including) versions 3.0 and 3.4
cannot be formatted using C{%} or C{format} so this does a simple
replace.
@type template: L{bytes}
@param template: A string containing "HOST".
@rtype: L{bytes}
@return: A string where "HOST" has been replaced by C{self.host}.
"""
self.assertIsInstance(self.host, bytes)
self.assertIsInstance(self.uriHost, bytes)
self.assertIsInstance(template, bytes)
self.assertIn(b"HOST", template)
return template.replace(b"HOST", self.uriHost)
def assertURIEquals(
self, uri, scheme, netloc, host, port, path, params=b"", query=b"", fragment=b""
):
"""
Assert that all of a L{client.URI}'s components match the expected
values.
@param uri: U{client.URI} instance whose attributes will be checked
for equality.
@type scheme: L{bytes}
@param scheme: URI scheme specifier.
@type netloc: L{bytes}
@param netloc: Network location component.
@type host: L{bytes}
@param host: Host name.
@type port: L{int}
@param port: Port number.
@type path: L{bytes}
@param path: Hierarchical path.
@type params: L{bytes}
@param params: Parameters for last path segment, defaults to C{b''}.
@type query: L{bytes}
@param query: Query string, defaults to C{b''}.
@type fragment: L{bytes}
@param fragment: Fragment identifier, defaults to C{b''}.
"""
self.assertEqual(
(scheme, netloc, host, port, path, params, query, fragment),
(
uri.scheme,
uri.netloc,
uri.host,
uri.port,
uri.path,
uri.params,
uri.query,
uri.fragment,
),
)
def test_parseDefaultPort(self):
"""
L{client.URI.fromBytes} by default assumes port 80 for the I{http}
scheme and 443 for the I{https} scheme.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST"))
self.assertEqual(80, uri.port)
# Weird (but commonly accepted) structure uses default port.
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST:"))
self.assertEqual(80, uri.port)
uri = client.URI.fromBytes(self.makeURIString(b"https://HOST"))
self.assertEqual(443, uri.port)
def test_parseCustomDefaultPort(self):
"""
L{client.URI.fromBytes} accepts a C{defaultPort} parameter that
overrides the normal default port logic.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST"), defaultPort=5144)
self.assertEqual(5144, uri.port)
uri = client.URI.fromBytes(
self.makeURIString(b"https://HOST"), defaultPort=5144
)
self.assertEqual(5144, uri.port)
def test_netlocHostPort(self):
"""
Parsing a I{URI} splits the network location component into I{host} and
I{port}.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST:5144"))
self.assertEqual(5144, uri.port)
self.assertEqual(self.host, uri.host)
self.assertEqual(self.uriHost + b":5144", uri.netloc)
# Spaces in the hostname are trimmed, the default path is /.
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST "))
self.assertEqual(self.uriHost, uri.netloc)
def test_path(self):
"""
Parse the path from a I{URI}.
"""
uri = self.makeURIString(b"http://HOST/foo/bar")
parsed = client.URI.fromBytes(uri)
self.assertURIEquals(
parsed,
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"/foo/bar",
)
self.assertEqual(uri, parsed.toBytes())
def test_noPath(self):
"""
The path of a I{URI} that has no path is the empty string.
"""
uri = self.makeURIString(b"http://HOST")
parsed = client.URI.fromBytes(uri)
self.assertURIEquals(
parsed,
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"",
)
self.assertEqual(uri, parsed.toBytes())
def test_emptyPath(self):
"""
The path of a I{URI} with an empty path is C{b'/'}.
"""
uri = self.makeURIString(b"http://HOST/")
self.assertURIEquals(
client.URI.fromBytes(uri),
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"/",
)
def test_param(self):
"""
Parse I{URI} parameters from a I{URI}.
"""
uri = self.makeURIString(b"http://HOST/foo/bar;param")
parsed = client.URI.fromBytes(uri)
self.assertURIEquals(
parsed,
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"/foo/bar",
params=b"param",
)
self.assertEqual(uri, parsed.toBytes())
def test_query(self):
"""
Parse the query string from a I{URI}.
"""
uri = self.makeURIString(b"http://HOST/foo/bar;param?a=1&b=2")
parsed = client.URI.fromBytes(uri)
self.assertURIEquals(
parsed,
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"/foo/bar",
params=b"param",
query=b"a=1&b=2",
)
self.assertEqual(uri, parsed.toBytes())
def test_fragment(self):
"""
Parse the fragment identifier from a I{URI}.
"""
uri = self.makeURIString(b"http://HOST/foo/bar;param?a=1&b=2#frag")
parsed = client.URI.fromBytes(uri)
self.assertURIEquals(
parsed,
scheme=b"http",
netloc=self.uriHost,
host=self.host,
port=80,
path=b"/foo/bar",
params=b"param",
query=b"a=1&b=2",
fragment=b"frag",
)
self.assertEqual(uri, parsed.toBytes())
def test_originForm(self):
"""
L{client.URI.originForm} produces an absolute I{URI} path including
the I{URI} path.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST/foo"))
self.assertEqual(b"/foo", uri.originForm)
def test_originFormComplex(self):
"""
L{client.URI.originForm} produces an absolute I{URI} path including
the I{URI} path, parameters and query string but excludes the fragment
identifier.
"""
uri = client.URI.fromBytes(
self.makeURIString(b"http://HOST/foo;param?a=1#frag")
)
self.assertEqual(b"/foo;param?a=1", uri.originForm)
def test_originFormNoPath(self):
"""
L{client.URI.originForm} produces a path of C{b'/'} when the I{URI}
specifies no path.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST"))
self.assertEqual(b"/", uri.originForm)
def test_originFormEmptyPath(self):
"""
L{client.URI.originForm} produces a path of C{b'/'} when the I{URI}
specifies an empty path.
"""
uri = client.URI.fromBytes(self.makeURIString(b"http://HOST/"))
self.assertEqual(b"/", uri.originForm)
def test_externalUnicodeInterference(self):
"""
L{client.URI.fromBytes} parses the scheme, host, and path elements
into L{bytes}, even when passed an URL which has previously been passed
to L{urlparse} as a L{unicode} string.
"""
goodInput = self.makeURIString(b"http://HOST/path")
badInput = goodInput.decode("ascii")
urlparse(badInput)
uri = client.URI.fromBytes(goodInput)
self.assertIsInstance(uri.scheme, bytes)
self.assertIsInstance(uri.host, bytes)
self.assertIsInstance(uri.path, bytes)
class URITestsForHostname(URITests, unittest.TestCase):
"""
Tests for L{twisted.web.client.URI} with host names.
"""
uriHost = host = b"example.com"
class URITestsForIPv4(URITests, unittest.TestCase):
"""
Tests for L{twisted.web.client.URI} with IPv4 host addresses.
"""
uriHost = host = b"192.168.1.67"
class URITestsForIPv6(URITests, unittest.TestCase):
"""
Tests for L{twisted.web.client.URI} with IPv6 host addresses.
IPv6 addresses must always be surrounded by square braces in URIs. No
attempt is made to test without.
"""
host = b"fe80::20c:29ff:fea4:c60"
uriHost = b"[fe80::20c:29ff:fea4:c60]"
def test_hostBracketIPv6AddressLiteral(self):
"""
Brackets around IPv6 addresses are stripped in the host field. The host
field is then exported with brackets in the output of
L{client.URI.toBytes}.
"""
uri = client.URI.fromBytes(b"http://[::1]:80/index.html")
self.assertEqual(uri.host, b"::1")
self.assertEqual(uri.netloc, b"[::1]:80")
self.assertEqual(uri.toBytes(), b"http://[::1]:80/index.html")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,949 @@
# -*- 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}.
"""
import datetime
from io import BytesIO, StringIO
from unittest import skipIf
from twisted.internet import defer, reactor
from twisted.internet.error import ConnectionDone
from twisted.internet.testing import EventLoggingObserver, MemoryReactor
from twisted.logger import (
FilteringLogObserver,
LogLevel,
LogLevelFilterPredicate,
globalLogPublisher,
)
from twisted.python import failure
from twisted.python.compat import nativeString, networkString
from twisted.python.reflect import namedModule
from twisted.trial import unittest
from twisted.web import client, http, server, static, xmlrpc
from twisted.web.test.test_web import DummyRequest
from twisted.web.xmlrpc import (
XMLRPC,
QueryFactory,
addIntrospection,
payloadTemplate,
withRequest,
xmlrpclib,
)
try:
namedModule("twisted.internet.ssl")
except ImportError:
sslSkip = True
else:
sslSkip = False
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 = StringIO(
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 = [ # type: ignore[attr-defined]
["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"]] # type: ignore[attr-defined]
# 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 = "\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." # type: ignore[attr-defined]
@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, f"procedure {procedureName} not found"
)
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", ("\u2603"), "\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", "\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)
@skipIf(sslSkip, "OpenSSL not present")
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)
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 = StringIO(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