Source code for cherrypy.test.test_request_obj

"""Basic tests for the cherrypy.Request object."""

from functools import wraps
import os
import sys
import types
import uuid
from http.client import IncompleteRead

import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.lib import httputil
from cherrypy.test import helper

localDir = os.path.dirname(__file__)

defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
                        'TRACE', 'PROPFIND', 'PATCH')


#                             Client-side code                             #


[docs]class RequestObjectTests(helper.CPWebCase):
[docs] @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return 'hello' @cherrypy.expose def scheme(self): return cherrypy.request.scheme @cherrypy.expose def created_example_com_3128(self): """Handle CONNECT method.""" cherrypy.response.status = 204 @cherrypy.expose def body_example_com_3128(self): """Handle CONNECT method.""" return ( cherrypy.request.method + 'ed to ' + cherrypy.request.path_info ) @cherrypy.expose def request_uuid4(self): return [ str(cherrypy.request.unique_id), ' ', str(cherrypy.request.unique_id), ] root = Root() class TestType(type): """Metaclass which automatically exposes all functions in each subclass, and adds an instance of the subclass as an attribute of root. """ def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in dct.values(): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) Test = TestType('Test', (object,), {}) class PathInfo(Test): def default(self, *args): return cherrypy.request.path_info class Params(Test): def index(self, thing): return repr(thing) def ismap(self, x, y): return 'Coordinates: %s, %s' % (x, y) @cherrypy.config(**{'request.query_string_encoding': 'latin1'}) def default(self, *args, **kwargs): return 'args: %s kwargs: %s' % (args, sorted(kwargs.items())) @cherrypy.expose class ParamErrorsCallable(object): def __call__(self): return 'data' def handler_dec(f): @wraps(f) def wrapper(handler, *args, **kwargs): return f(handler, *args, **kwargs) return wrapper class ParamErrors(Test): @cherrypy.expose def one_positional(self, param1): return 'data' @cherrypy.expose def one_positional_args(self, param1, *args): return 'data' @cherrypy.expose def one_positional_args_kwargs(self, param1, *args, **kwargs): return 'data' @cherrypy.expose def one_positional_kwargs(self, param1, **kwargs): return 'data' @cherrypy.expose def no_positional(self): return 'data' @cherrypy.expose def no_positional_args(self, *args): return 'data' @cherrypy.expose def no_positional_args_kwargs(self, *args, **kwargs): return 'data' @cherrypy.expose def no_positional_kwargs(self, **kwargs): return 'data' callable_object = ParamErrorsCallable() @cherrypy.expose def raise_type_error(self, **kwargs): raise TypeError('Client Error') @cherrypy.expose def raise_type_error_with_default_param(self, x, y=None): return '%d' % 'a' # throw an exception @cherrypy.expose @handler_dec def raise_type_error_decorated(self, *args, **kwargs): raise TypeError('Client Error') def callable_error_page(status, **kwargs): return "Error %s - Well, I'm very sorry but you haven't paid!" % ( status) @cherrypy.config(**{'tools.log_tracebacks.on': True}) class Error(Test): def reason_phrase(self): raise cherrypy.HTTPError("410 Gone fishin'") @cherrypy.config(**{ 'error_page.404': os.path.join(localDir, 'static/index.html'), 'error_page.401': callable_error_page, }) def custom(self, err='404'): raise cherrypy.HTTPError( int(err), 'No, <b>really</b>, not found!') @cherrypy.config(**{ 'error_page.default': callable_error_page, }) def custom_default(self): return 1 + 'a' # raise an unexpected error @cherrypy.config(**{'error_page.404': 'nonexistent.html'}) def noexist(self): raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!') def page_method(self): raise ValueError() def page_yield(self): yield 'howdy' raise ValueError() @cherrypy.config(**{'response.stream': True}) def page_streamed(self): yield 'word up' raise ValueError() yield 'very oops' @cherrypy.config(**{'request.show_tracebacks': False}) def cause_err_in_finalize(self): # Since status must start with an int, this should error. cherrypy.response.status = 'ZOO OK' @cherrypy.config(**{'request.throw_errors': True}) def rethrow(self): """Test that an error raised here will be thrown out to the server. """ raise ValueError() class Expect(Test): def expectation_failed(self): expect = cherrypy.request.headers.elements('Expect') if expect and expect[0].value != '100-continue': raise cherrypy.HTTPError(400) raise cherrypy.HTTPError(417, 'Expectation Failed') class Headers(Test): def default(self, headername): """Spit back out the value for the requested header.""" return cherrypy.request.headers[headername] def doubledheaders(self): # From https://github.com/cherrypy/cherrypy/issues/165: # "header field names should not be case sensitive sayes the # rfc. if i set a headerfield in complete lowercase i end up # with two header fields, one in lowercase, the other in # mixed-case." # Set the most common headers hMap = cherrypy.response.headers hMap['content-type'] = 'text/html' hMap['content-length'] = 18 hMap['server'] = 'CherryPy headertest' hMap['location'] = ('%s://%s:%s/headers/' % (cherrypy.request.local.ip, cherrypy.request.local.port, cherrypy.request.scheme)) # Set a rare header for fun hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' return 'double header test' def ifmatch(self): val = cherrypy.request.headers['If-Match'] assert isinstance(val, str) cherrypy.response.headers['ETag'] = val return val class HeaderElements(Test): def get_elements(self, headername): e = cherrypy.request.headers.elements(headername) return '\n'.join([str(x) for x in e]) class Method(Test): def index(self): m = cherrypy.request.method if m in defined_http_methods or m == 'CONNECT': return m if m == 'LINK': raise cherrypy.HTTPError(405) else: raise cherrypy.HTTPError(501) def parameterized(self, data): return data def request_body(self): # This should be a file object (temp file), # which CP will just pipe back out if we tell it to. return cherrypy.request.body def reachable(self): return 'success' class Divorce(Test): """HTTP Method handlers shouldn't collide with normal method names. For example, a GET-handler shouldn't collide with a method named 'get'. If you build HTTP method dispatching into CherryPy, rewrite this class to use your new dispatch mechanism and make sure that: "GET /divorce HTTP/1.1" maps to divorce.index() and "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() """ documents = {} @cherrypy.expose def index(self): yield '<h1>Choose your document</h1>\n' yield '<ul>\n' for id, contents in self.documents.items(): yield ( " <li><a href='/divorce/get?ID=%s'>%s</a>:" ' %s</li>\n' % (id, id, contents)) yield '</ul>' @cherrypy.expose def get(self, ID): return ('Divorce document %s: %s' % (ID, self.documents.get(ID, 'empty'))) class ThreadLocal(Test): def index(self): existing = repr(getattr(cherrypy.request, 'asdf', None)) cherrypy.request.asdf = 'rassfrassin' return existing appconf = { '/method': { 'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND', 'PATCH') }, } cherrypy.tree.mount(root, config=appconf)
[docs] def test_scheme(self): self.getPage('/scheme') self.assertBody(self.scheme)
[docs] def test_per_request_uuid4(self): self.getPage('/request_uuid4') first_uuid4, _, second_uuid4 = self.body.decode().partition(' ') assert ( uuid.UUID(first_uuid4, version=4) == uuid.UUID(second_uuid4, version=4) ) self.getPage('/request_uuid4') third_uuid4, _, _ = self.body.decode().partition(' ') assert ( uuid.UUID(first_uuid4, version=4) != uuid.UUID(third_uuid4, version=4) )
[docs] def testRelativeURIPathInfo(self): self.getPage('/pathinfo/foo/bar') self.assertBody('/pathinfo/foo/bar')
[docs] def testAbsoluteURIPathInfo(self): # http://cherrypy.dev/ticket/1061 self.getPage('http://localhost/pathinfo/foo/bar') self.assertBody('/pathinfo/foo/bar')
[docs] def testParams(self): self.getPage('/params/?thing=a') self.assertBody(repr(ntou('a'))) self.getPage('/params/?thing=a&thing=b&thing=c') self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) # Test friendly error message when given params are not accepted. cherrypy.config.update({'request.show_mismatched_params': True}) self.getPage('/params/?notathing=meeting') self.assertInBody('Missing parameters: thing') self.getPage('/params/?thing=meeting&notathing=meeting') self.assertInBody('Unexpected query string parameters: notathing') # Test ability to turn off friendly error messages cherrypy.config.update({'request.show_mismatched_params': False}) self.getPage('/params/?notathing=meeting') self.assertInBody('Not Found') self.getPage('/params/?thing=meeting&notathing=meeting') self.assertInBody('Not Found') # Test "% HEX HEX"-encoded URL, param keys, and values self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville') self.assertBody('args: %s kwargs: %s' % (('\xd4 \xe3', 'cheese'), [('Gruy\xe8re', ntou('Bulgn\xe9ville'))])) # Make sure that encoded = and & get parsed correctly self.getPage( '/params/code?url=http%3A//cherrypy.dev/index%3Fa%3D1%26b%3D2') self.assertBody('args: %s kwargs: %s' % (('code',), [('url', ntou('http://cherrypy.dev/index?a=1&b=2'))])) # Test coordinates sent by <img ismap> self.getPage('/params/ismap?223,114') self.assertBody('Coordinates: 223, 114') # Test "name[key]" dict-like params self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz') self.assertBody('args: %s kwargs: %s' % (('dictlike',), [('a[1]', ntou('1')), ('a[2]', ntou('2')), ('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
[docs] def testParamErrors(self): # test that all of the handlers work when given # the correct parameters in order to ensure that the # errors below aren't coming from some other source. for uri in ( '/paramerrors/one_positional?param1=foo', '/paramerrors/one_positional_args?param1=foo', '/paramerrors/one_positional_args/foo', '/paramerrors/one_positional_args/foo/bar/baz', '/paramerrors/one_positional_args_kwargs?' 'param1=foo&param2=bar', '/paramerrors/one_positional_args_kwargs/foo?' 'param2=bar&param3=baz', '/paramerrors/one_positional_args_kwargs/foo/bar/baz?' 'param2=bar&param3=baz', '/paramerrors/one_positional_kwargs?' 'param1=foo&param2=bar&param3=baz', '/paramerrors/one_positional_kwargs/foo?' 'param4=foo&param2=bar&param3=baz', '/paramerrors/no_positional', '/paramerrors/no_positional_args/foo', '/paramerrors/no_positional_args/foo/bar/baz', '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar', '/paramerrors/no_positional_args_kwargs/foo?param2=bar', '/paramerrors/no_positional_args_kwargs/foo/bar/baz?' 'param2=bar&param3=baz', '/paramerrors/no_positional_kwargs?param1=foo&param2=bar', '/paramerrors/callable_object', ): self.getPage(uri) self.assertStatus(200) error_msgs = [ 'Missing parameters', 'Nothing matches the given URI', 'Multiple values for parameters', 'Unexpected query string parameters', 'Unexpected body parameters', 'Invalid path in Request-URI', 'Illegal #fragment in Request-URI', ] # uri should be tested for valid absolute path, the status must be 400. for uri, error_idx in ( ('invalid/path/without/leading/slash', 5), ('/valid/path#invalid=fragment', 6), ): self.getPage(uri) self.assertStatus(400) self.assertInBody(error_msgs[error_idx]) # query string parameters are part of the URI, so if they are wrong # for a particular handler, the status MUST be a 404. for uri, msg in ( ('/paramerrors/one_positional', error_msgs[0]), ('/paramerrors/one_positional?foo=foo', error_msgs[0]), ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]), ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]), ('/paramerrors/one_positional/foo?param1=foo&param2=foo', error_msgs[2]), ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo', error_msgs[2]), ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]), ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?' 'param1=bar&param3=baz', error_msgs[2]), ('/paramerrors/one_positional_kwargs/foo?' 'param1=foo&param2=bar&param3=baz', error_msgs[2]), ('/paramerrors/no_positional/boo', error_msgs[1]), ('/paramerrors/no_positional?param1=foo', error_msgs[3]), ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]), ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]), ('/paramerrors/callable_object?param1=foo', error_msgs[3]), ('/paramerrors/callable_object/boo', error_msgs[1]), ): for show_mismatched_params in (True, False): cherrypy.config.update( {'request.show_mismatched_params': show_mismatched_params}) self.getPage(uri) self.assertStatus(404) if show_mismatched_params: self.assertInBody(msg) else: self.assertInBody('Not Found') # if body parameters are wrong, a 400 must be returned. for uri, body, msg in ( ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]), ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo', error_msgs[2]), ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo', error_msgs[2]), ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]), ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz', error_msgs[2]), ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz', error_msgs[2]), ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]), ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), ): for show_mismatched_params in (True, False): cherrypy.config.update( {'request.show_mismatched_params': show_mismatched_params}) self.getPage(uri, method='POST', body=body) self.assertStatus(400) if show_mismatched_params: self.assertInBody(msg) else: self.assertInBody('400 Bad') # even if body parameters are wrong, if we get the uri wrong, then # it's a 404 for uri, body, msg in ( ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]), ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]), ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]), ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz', error_msgs[1]), ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]), ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]), ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]), ): for show_mismatched_params in (True, False): cherrypy.config.update( {'request.show_mismatched_params': show_mismatched_params}) self.getPage(uri, method='POST', body=body) self.assertStatus(404) if show_mismatched_params: self.assertInBody(msg) else: self.assertInBody('Not Found') # In the case that a handler raises a TypeError we should # let that type error through. for uri in ( '/paramerrors/raise_type_error', '/paramerrors/raise_type_error_with_default_param?x=0', '/paramerrors/raise_type_error_with_default_param?x=0&y=0', '/paramerrors/raise_type_error_decorated', ): self.getPage(uri, method='GET') self.assertStatus(500) self.assertTrue('Client Error', self.body)
[docs] def testErrorHandling(self): self.getPage('/error/missing') self.assertStatus(404) self.assertErrorPage(404, "The path '/error/missing' was not found.") ignore = helper.webtest.ignored_exceptions ignore.append(ValueError) try: valerr = '\n raise ValueError()\nValueError' self.getPage('/error/page_method') self.assertErrorPage(500, pattern=valerr) self.getPage('/error/page_yield') self.assertErrorPage(500, pattern=valerr) if (cherrypy.server.protocol_version == 'HTTP/1.0' or getattr(cherrypy.server, 'using_apache', False)): self.getPage('/error/page_streamed') # Because this error is raised after the response body has # started, the status should not change to an error status. self.assertStatus(200) self.assertBody('word up') else: # Under HTTP/1.1, the chunked transfer-coding is used. # The HTTP client will choke when the output is incomplete. self.assertRaises((ValueError, IncompleteRead), self.getPage, '/error/page_streamed') # No traceback should be present self.getPage('/error/cause_err_in_finalize') msg = "Illegal response status from server ('ZOO' is non-numeric)." self.assertErrorPage(500, msg, None) finally: ignore.pop() # Test HTTPError with a reason-phrase in the status arg. self.getPage('/error/reason_phrase') self.assertStatus("410 Gone fishin'") # Test custom error page for a specific error. self.getPage('/error/custom') self.assertStatus(404) self.assertBody('Hello, world\r\n' + (' ' * 499)) # Test custom error page for a specific error. self.getPage('/error/custom?err=401') self.assertStatus(401) self.assertBody( 'Error 401 Unauthorized - ' "Well, I'm very sorry but you haven't paid!") # Test default custom error page. self.getPage('/error/custom_default') self.assertStatus(500) self.assertBody( 'Error 500 Internal Server Error - ' "Well, I'm very sorry but you haven't paid!".ljust(513)) # Test error in custom error page (ticket #305). # Note that the message is escaped for HTML (ticket #310). self.getPage('/error/noexist') self.assertStatus(404) if sys.version_info >= (3, 3): exc_name = 'FileNotFoundError' else: exc_name = 'IOError' msg = ('No, &lt;b&gt;really&lt;/b&gt;, not found!<br />' 'In addition, the custom error page failed:\n<br />' '%s: [Errno 2] ' "No such file or directory: 'nonexistent.html'") % (exc_name,) self.assertInBody(msg) if getattr(cherrypy.server, 'using_apache', False): pass else: # Test throw_errors (ticket #186). self.getPage('/error/rethrow') self.assertInBody('raise ValueError()')
[docs] def testExpect(self): e = ('Expect', '100-continue') self.getPage('/headerelements/get_elements?headername=Expect', [e]) self.assertBody('100-continue') self.getPage('/expect/expectation_failed', [e]) self.assertStatus(417)
[docs] def testHeaderElements(self): # Accept-* header elements should be sorted, with most preferred first. h = [('Accept', 'audio/*; q=0.2, audio/basic')] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) self.assertBody('audio/basic\n' 'audio/*;q=0.2') h = [ ('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c') ] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) self.assertBody('text/x-c\n' 'text/html\n' 'text/x-dvi;q=0.8\n' 'text/plain;q=0.5') # Test that more specific media ranges get priority. h = [('Accept', 'text/*, text/html, text/html;level=1, */*')] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) self.assertBody('text/html;level=1\n' 'text/html\n' 'text/*\n' '*/*') # Test Accept-Charset h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] self.getPage( '/headerelements/get_elements?headername=Accept-Charset', h) self.assertStatus('200 OK') self.assertBody('iso-8859-5\n' 'unicode-1-1;q=0.8') # Test Accept-Encoding h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')] self.getPage( '/headerelements/get_elements?headername=Accept-Encoding', h) self.assertStatus('200 OK') self.assertBody('gzip;q=1.0\n' 'identity;q=0.5\n' '*;q=0') # Test Accept-Language h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')] self.getPage( '/headerelements/get_elements?headername=Accept-Language', h) self.assertStatus('200 OK') self.assertBody('da\n' 'en-gb;q=0.8\n' 'en;q=0.7') # Test malformed header parsing. See # https://github.com/cherrypy/cherrypy/issues/763. self.getPage('/headerelements/get_elements?headername=Content-Type', # Note the illegal trailing ";" headers=[('Content-Type', 'text/html; charset=utf-8;')]) self.assertStatus(200) self.assertBody('text/html;charset=utf-8')
[docs] def test_repeated_headers(self): # Test that two request headers are collapsed into one. # See https://github.com/cherrypy/cherrypy/issues/542. self.getPage('/headers/Accept-Charset', headers=[('Accept-Charset', 'iso-8859-5'), ('Accept-Charset', 'unicode-1-1;q=0.8')]) self.assertBody('iso-8859-5, unicode-1-1;q=0.8') # Tests that each header only appears once, regardless of case. self.getPage('/headers/doubledheaders') self.assertBody('double header test') hnames = [name.title() for name, val in self.headers] for key in ['Content-Length', 'Content-Type', 'Date', 'Expires', 'Location', 'Server']: self.assertEqual(hnames.count(key), 1, self.headers)
[docs] def test_encoded_headers(self): # First, make sure the innards work like expected. self.assertEqual( httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr')) if cherrypy.server.protocol_version == 'HTTP/1.1': # Test RFC-2047-encoded request and response header values u = ntou('\u212bngstr\xf6m', 'escape') c = ntou('=E2=84=ABngstr=C3=B6m') self.getPage('/headers/ifmatch', [('If-Match', ntou('=?utf-8?q?%s?=') % c)]) # The body should be utf-8 encoded. self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m') # But the Etag header should be RFC-2047 encoded (binary) self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?=')) # Test a *LONG* RFC-2047-encoded request and response header value self.getPage('/headers/ifmatch', [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))]) self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10) # Note: this is different output for Python3, but it decodes fine. etag = self.assertHeader( 'ETag', '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' '4oSrbmdzdHLDtm0=?=') self.assertEqual(httputil.decode_TEXT(etag), u * 10)
[docs] def test_header_presence(self): # If we don't pass a Content-Type header, it should not be present # in cherrypy.request.headers self.getPage('/headers/Content-Type', headers=[]) self.assertStatus(500) # If Content-Type is present in the request, it should be present in # cherrypy.request.headers self.getPage('/headers/Content-Type', headers=[('Content-type', 'application/json')]) self.assertBody('application/json')
[docs] def test_dangerous_host(self): """Dangerous characters like newlines should be elided. Ref #1974. """ # foo\nbar encoded = '=?iso-8859-1?q?foo=0Abar?=' self.getPage('/headers/Host', headers=[('Host', encoded)]) self.assertBody('foobar')
[docs] def test_basic_HTTPMethods(self): helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND', 'PATCH') # Test that all defined HTTP methods work. for m in defined_http_methods: self.getPage('/method/', method=m) # HEAD requests should not return any body. if m == 'HEAD': self.assertBody('') elif m == 'TRACE': # Some HTTP servers (like modpy) have their own TRACE support self.assertEqual(self.body[:5], b'TRACE') else: self.assertBody(m) # test of PATCH requests # Request a PATCH method with a form-urlencoded body self.getPage('/method/parameterized', method='PATCH', body='data=on+top+of+other+things') self.assertBody('on top of other things') # Request a PATCH method with a file body b = 'one thing on top of another' h = [('Content-Type', 'text/plain'), ('Content-Length', str(len(b)))] self.getPage('/method/request_body', headers=h, method='PATCH', body=b) self.assertStatus(200) self.assertBody(b) # Request a PATCH method with a file body but no Content-Type. # See https://github.com/cherrypy/cherrypy/issues/790. b = b'one thing on top of another' self.persistent = True try: conn = self.HTTP_CONN conn.putrequest('PATCH', '/method/request_body', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Length', str(len(b))) conn.endheaders() conn.send(b) response = conn.response_class(conn.sock, method='PATCH') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(b) finally: self.persistent = False # Request a PATCH method with no body whatsoever (not an empty one). # See https://github.com/cherrypy/cherrypy/issues/650. # Provide a C-T or webtest will provide one (and a C-L) for us. h = [('Content-Type', 'text/plain')] self.getPage('/method/reachable', headers=h, method='PATCH') self.assertStatus(411) # HTTP PUT tests # Request a PUT method with a form-urlencoded body self.getPage('/method/parameterized', method='PUT', body='data=on+top+of+other+things') self.assertBody('on top of other things') # Request a PUT method with a file body b = 'one thing on top of another' h = [('Content-Type', 'text/plain'), ('Content-Length', str(len(b)))] self.getPage('/method/request_body', headers=h, method='PUT', body=b) self.assertStatus(200) self.assertBody(b) # Request a PUT method with a file body but no Content-Type. # See https://github.com/cherrypy/cherrypy/issues/790. b = b'one thing on top of another' self.persistent = True try: conn = self.HTTP_CONN conn.putrequest('PUT', '/method/request_body', skip_host=True) conn.putheader('Host', self.HOST) conn.putheader('Content-Length', str(len(b))) conn.endheaders() conn.send(b) response = conn.response_class(conn.sock, method='PUT') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(b) finally: self.persistent = False # Request a PUT method with no body whatsoever (not an empty one). # See https://github.com/cherrypy/cherrypy/issues/650. # Provide a C-T or webtest will provide one (and a C-L) for us. h = [('Content-Type', 'text/plain')] self.getPage('/method/reachable', headers=h, method='PUT') self.assertStatus(411) # Request a custom method with a request body b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n' '<propfind xmlns="DAV:"><prop><getlastmodified/>' '</prop></propfind>') h = [('Content-Type', 'text/xml'), ('Content-Length', str(len(b)))] self.getPage('/method/request_body', headers=h, method='PROPFIND', body=b) self.assertStatus(200) self.assertBody(b) # Request a disallowed method self.getPage('/method/', method='LINK') self.assertStatus(405) # Request an unknown method self.getPage('/method/', method='SEARCH') self.assertStatus(501) # For method dispatchers: make sure that an HTTP method doesn't # collide with a virtual path atom. If you build HTTP-method # dispatching into the core, rewrite these handlers to use # your dispatch idioms. self.getPage('/divorce/get?ID=13') self.assertBody('Divorce document 13: empty') self.assertStatus(200) self.getPage('/divorce/', method='GET') self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>') self.assertStatus(200)
[docs] def test_CONNECT_method(self): self.persistent = True try: conn = self.HTTP_CONN conn.request('CONNECT', 'created.example.com:3128') response = conn.response_class(conn.sock, method='CONNECT') response.begin() self.assertEqual(response.status, 204) finally: self.persistent = False self.persistent = True try: conn = self.HTTP_CONN conn.request('CONNECT', 'body.example.com:3128') response = conn.response_class(conn.sock, method='CONNECT') response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(b'CONNECTed to /body.example.com:3128') finally: self.persistent = False
[docs] def test_CONNECT_method_invalid_authority(self): for request_target in ['example.com', 'http://example.com:33', '/path/', 'path/', '/?q=f', '#f']: self.persistent = True try: conn = self.HTTP_CONN conn.request('CONNECT', request_target) response = conn.response_class(conn.sock, method='CONNECT') response.begin() self.assertEqual(response.status, 400) self.body = response.read() self.assertBody(b'Invalid path in Request-URI: request-target ' b'must match authority-form.') finally: self.persistent = False
[docs] def testEmptyThreadlocals(self): results = [] for x in range(20): self.getPage('/threadlocal/') results.append(self.body) self.assertEqual(results, [b'None'] * 20)