Update cherrpy to 17.4.2
This commit is contained in:
24
lib/cherrypy/test/__init__.py
Normal file
24
lib/cherrypy/test/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Regression test suite for CherryPy.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def newexit():
|
||||
os._exit(1)
|
||||
|
||||
|
||||
def setup():
|
||||
# We want to monkey patch sys.exit so that we can get some
|
||||
# information about where exit is being called.
|
||||
newexit._old = sys.exit
|
||||
sys.exit = newexit
|
||||
|
||||
|
||||
def teardown():
|
||||
try:
|
||||
sys.exit = sys.exit._old
|
||||
except AttributeError:
|
||||
sys.exit = sys._exit
|
||||
39
lib/cherrypy/test/_test_decorators.py
Normal file
39
lib/cherrypy/test/_test_decorators.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Test module for the @-decorator syntax, which is version-specific"""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import expose, tools
|
||||
|
||||
|
||||
class ExposeExamples(object):
|
||||
|
||||
@expose
|
||||
def no_call(self):
|
||||
return 'Mr E. R. Bradshaw'
|
||||
|
||||
@expose()
|
||||
def call_empty(self):
|
||||
return 'Mrs. B.J. Smegma'
|
||||
|
||||
@expose('call_alias')
|
||||
def nesbitt(self):
|
||||
return 'Mr Nesbitt'
|
||||
|
||||
@expose(['alias1', 'alias2'])
|
||||
def andrews(self):
|
||||
return 'Mr Ken Andrews'
|
||||
|
||||
@expose(alias='alias3')
|
||||
def watson(self):
|
||||
return 'Mr. and Mrs. Watson'
|
||||
|
||||
|
||||
class ToolExamples(object):
|
||||
|
||||
@expose
|
||||
# This is here to demonstrate that using the config decorator
|
||||
# does not overwrite other config attributes added by the Tool
|
||||
# decorator (in this case response_headers).
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
@tools.response_headers(headers=[('Content-Type', 'application/data')])
|
||||
def blah(self):
|
||||
yield b'blah'
|
||||
69
lib/cherrypy/test/_test_states_demo.py
Normal file
69
lib/cherrypy/test/_test_states_demo.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
|
||||
starttime = time.time()
|
||||
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hello World'
|
||||
|
||||
@cherrypy.expose
|
||||
def mtimes(self):
|
||||
return repr(cherrypy.engine.publish('Autoreloader', 'mtimes'))
|
||||
|
||||
@cherrypy.expose
|
||||
def pid(self):
|
||||
return str(os.getpid())
|
||||
|
||||
@cherrypy.expose
|
||||
def start(self):
|
||||
return repr(starttime)
|
||||
|
||||
@cherrypy.expose
|
||||
def exit(self):
|
||||
# This handler might be called before the engine is STARTED if an
|
||||
# HTTP worker thread handles it before the HTTP server returns
|
||||
# control to engine.start. We avoid that race condition here
|
||||
# by waiting for the Bus to be STARTED.
|
||||
cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
|
||||
cherrypy.engine.exit()
|
||||
|
||||
|
||||
@cherrypy.engine.subscribe('start', priority=100)
|
||||
def unsub_sig():
|
||||
cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False))
|
||||
if cherrypy.config.get('unsubsig', False):
|
||||
cherrypy.log('Unsubscribing the default cherrypy signal handler')
|
||||
cherrypy.engine.signal_handler.unsubscribe()
|
||||
try:
|
||||
from signal import signal, SIGTERM
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
def old_term_handler(signum=None, frame=None):
|
||||
cherrypy.log('I am an old SIGTERM handler.')
|
||||
sys.exit(0)
|
||||
cherrypy.log('Subscribing the new one.')
|
||||
signal(SIGTERM, old_term_handler)
|
||||
|
||||
|
||||
@cherrypy.engine.subscribe('start', priority=6)
|
||||
def starterror():
|
||||
if cherrypy.config.get('starterror', False):
|
||||
1 / 0
|
||||
|
||||
|
||||
@cherrypy.engine.subscribe('start', priority=6)
|
||||
def log_test_case_name():
|
||||
if cherrypy.config.get('test_case_name', False):
|
||||
cherrypy.log('STARTED FROM: %s' %
|
||||
cherrypy.config.get('test_case_name'))
|
||||
|
||||
|
||||
cherrypy.tree.mount(Root(), '/', {'/': {}})
|
||||
425
lib/cherrypy/test/benchmark.py
Normal file
425
lib/cherrypy/test/benchmark.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""CherryPy Benchmark Tool
|
||||
|
||||
Usage:
|
||||
benchmark.py [options]
|
||||
|
||||
--null: use a null Request object (to bench the HTTP server only)
|
||||
--notests: start the server but do not run the tests; this allows
|
||||
you to check the tested pages with a browser
|
||||
--help: show this help message
|
||||
--cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy)
|
||||
--modpython: run tests via apache on 54583 (with modpython_gateway)
|
||||
--ab=path: Use the ab script/executable at 'path' (see below)
|
||||
--apache=path: Use the apache script/exe at 'path' (see below)
|
||||
|
||||
To run the benchmarks, the Apache Benchmark tool "ab" must either be on
|
||||
your system path, or specified via the --ab=path option.
|
||||
|
||||
To run the modpython tests, the "apache" executable or script must be
|
||||
on your system path, or provided via the --apache=path option. On some
|
||||
platforms, "apache" may be called "apachectl" or "apache2ctl"--create
|
||||
a symlink to them if needed.
|
||||
"""
|
||||
|
||||
import getopt
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cperror, _cpmodpy
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
AB_PATH = ''
|
||||
APACHE_PATH = 'apache'
|
||||
SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog'
|
||||
|
||||
__all__ = ['ABSession', 'Root', 'print_report',
|
||||
'run_standard_benchmarks', 'safe_threads',
|
||||
'size_report', 'thread_report',
|
||||
]
|
||||
|
||||
size_cache = {}
|
||||
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return """<html>
|
||||
<head>
|
||||
<title>CherryPy Benchmark</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
|
||||
<li><a href="static/index.html">Static file (14 bytes static)</a></li>
|
||||
<li><form action="sizer">Response of length:
|
||||
<input type='text' name='size' value='10' /></form>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
@cherrypy.expose
|
||||
def hello(self):
|
||||
return 'Hello, world\r\n'
|
||||
|
||||
@cherrypy.expose
|
||||
def sizer(self, size):
|
||||
resp = size_cache.get(size, None)
|
||||
if resp is None:
|
||||
size_cache[size] = resp = 'X' * int(size)
|
||||
return resp
|
||||
|
||||
|
||||
def init():
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.error.file': '',
|
||||
'environment': 'production',
|
||||
'server.socket_host': '127.0.0.1',
|
||||
'server.socket_port': 54583,
|
||||
'server.max_request_header_size': 0,
|
||||
'server.max_request_body_size': 0,
|
||||
})
|
||||
|
||||
# Cheat mode on ;)
|
||||
del cherrypy.config['tools.log_tracebacks.on']
|
||||
del cherrypy.config['tools.log_headers.on']
|
||||
del cherrypy.config['tools.trailing_slash.on']
|
||||
|
||||
appconf = {
|
||||
'/static': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.root': curdir,
|
||||
},
|
||||
}
|
||||
globals().update(
|
||||
app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf),
|
||||
)
|
||||
|
||||
|
||||
class NullRequest:
|
||||
|
||||
"""A null HTTP request class, returning 200 and an empty body."""
|
||||
|
||||
def __init__(self, local, remote, scheme='http'):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def run(self, method, path, query_string, protocol, headers, rfile):
|
||||
cherrypy.response.status = '200 OK'
|
||||
cherrypy.response.header_list = [('Content-Type', 'text/html'),
|
||||
('Server', 'Null CherryPy'),
|
||||
('Date', httputil.HTTPDate()),
|
||||
('Content-Length', '0'),
|
||||
]
|
||||
cherrypy.response.body = ['']
|
||||
return cherrypy.response
|
||||
|
||||
|
||||
class NullResponse:
|
||||
pass
|
||||
|
||||
|
||||
class ABSession:
|
||||
|
||||
"""A session of 'ab', the Apache HTTP server benchmarking tool.
|
||||
|
||||
Example output from ab:
|
||||
|
||||
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
|
||||
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 127.0.0.1 (be patient)
|
||||
Completed 100 requests
|
||||
Completed 200 requests
|
||||
Completed 300 requests
|
||||
Completed 400 requests
|
||||
Completed 500 requests
|
||||
Completed 600 requests
|
||||
Completed 700 requests
|
||||
Completed 800 requests
|
||||
Completed 900 requests
|
||||
|
||||
|
||||
Server Software: CherryPy/3.1beta
|
||||
Server Hostname: 127.0.0.1
|
||||
Server Port: 54583
|
||||
|
||||
Document Path: /static/index.html
|
||||
Document Length: 14 bytes
|
||||
|
||||
Concurrency Level: 10
|
||||
Time taken for tests: 9.643867 seconds
|
||||
Complete requests: 1000
|
||||
Failed requests: 0
|
||||
Write errors: 0
|
||||
Total transferred: 189000 bytes
|
||||
HTML transferred: 14000 bytes
|
||||
Requests per second: 103.69 [#/sec] (mean)
|
||||
Time per request: 96.439 [ms] (mean)
|
||||
Time per request: 9.644 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 19.08 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 2.9 0 10
|
||||
Processing: 20 94 7.3 90 130
|
||||
Waiting: 0 43 28.1 40 100
|
||||
Total: 20 95 7.3 100 130
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 100
|
||||
66% 100
|
||||
75% 100
|
||||
80% 100
|
||||
90% 100
|
||||
95% 100
|
||||
98% 100
|
||||
99% 110
|
||||
100% 130 (longest request)
|
||||
Finished 1000 requests
|
||||
"""
|
||||
|
||||
parse_patterns = [
|
||||
('complete_requests', 'Completed',
|
||||
br'^Complete requests:\s*(\d+)'),
|
||||
('failed_requests', 'Failed',
|
||||
br'^Failed requests:\s*(\d+)'),
|
||||
('requests_per_second', 'req/sec',
|
||||
br'^Requests per second:\s*([0-9.]+)'),
|
||||
('time_per_request_concurrent', 'msec/req',
|
||||
br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'),
|
||||
('transfer_rate', 'KB/sec',
|
||||
br'^Transfer rate:\s*([0-9.]+)')
|
||||
]
|
||||
|
||||
def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000,
|
||||
concurrency=10):
|
||||
self.path = path
|
||||
self.requests = requests
|
||||
self.concurrency = concurrency
|
||||
|
||||
def args(self):
|
||||
port = cherrypy.server.socket_port
|
||||
assert self.concurrency > 0
|
||||
assert self.requests > 0
|
||||
# Don't use "localhost".
|
||||
# Cf
|
||||
# http://mail.python.org/pipermail/python-win32/2008-March/007050.html
|
||||
return ('-k -n %s -c %s http://127.0.0.1:%s%s' %
|
||||
(self.requests, self.concurrency, port, self.path))
|
||||
|
||||
def run(self):
|
||||
# Parse output of ab, setting attributes on self
|
||||
try:
|
||||
self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args())
|
||||
except Exception:
|
||||
print(_cperror.format_exc())
|
||||
raise
|
||||
|
||||
for attr, name, pattern in self.parse_patterns:
|
||||
val = re.search(pattern, self.output, re.MULTILINE)
|
||||
if val:
|
||||
val = val.group(1)
|
||||
setattr(self, attr, val)
|
||||
else:
|
||||
setattr(self, attr, None)
|
||||
|
||||
|
||||
safe_threads = (25, 50, 100, 200, 400)
|
||||
if sys.platform in ('win32',):
|
||||
# For some reason, ab crashes with > 50 threads on my Win2k laptop.
|
||||
safe_threads = (10, 20, 30, 40, 50)
|
||||
|
||||
|
||||
def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads):
|
||||
sess = ABSession(path)
|
||||
attrs, names, patterns = list(zip(*sess.parse_patterns))
|
||||
avg = dict.fromkeys(attrs, 0.0)
|
||||
|
||||
yield ('threads',) + names
|
||||
for c in concurrency:
|
||||
sess.concurrency = c
|
||||
sess.run()
|
||||
row = [c]
|
||||
for attr in attrs:
|
||||
val = getattr(sess, attr)
|
||||
if val is None:
|
||||
print(sess.output)
|
||||
row = None
|
||||
break
|
||||
val = float(val)
|
||||
avg[attr] += float(val)
|
||||
row.append(val)
|
||||
if row:
|
||||
yield row
|
||||
|
||||
# Add a row of averages.
|
||||
yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs]
|
||||
|
||||
|
||||
def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
|
||||
concurrency=50):
|
||||
sess = ABSession(concurrency=concurrency)
|
||||
attrs, names, patterns = list(zip(*sess.parse_patterns))
|
||||
yield ('bytes',) + names
|
||||
for sz in sizes:
|
||||
sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz)
|
||||
sess.run()
|
||||
yield [sz] + [getattr(sess, attr) for attr in attrs]
|
||||
|
||||
|
||||
def print_report(rows):
|
||||
for row in rows:
|
||||
print('')
|
||||
for val in row:
|
||||
sys.stdout.write(str(val).rjust(10) + ' | ')
|
||||
print('')
|
||||
|
||||
|
||||
def run_standard_benchmarks():
|
||||
print('')
|
||||
print('Client Thread Report (1000 requests, 14 byte response body, '
|
||||
'%s server threads):' % cherrypy.server.thread_pool)
|
||||
print_report(thread_report())
|
||||
|
||||
print('')
|
||||
print('Client Thread Report (1000 requests, 14 bytes via staticdir, '
|
||||
'%s server threads):' % cherrypy.server.thread_pool)
|
||||
print_report(thread_report('%s/static/index.html' % SCRIPT_NAME))
|
||||
|
||||
print('')
|
||||
print('Size Report (1000 requests, 50 client threads, '
|
||||
'%s server threads):' % cherrypy.server.thread_pool)
|
||||
print_report(size_report())
|
||||
|
||||
|
||||
# modpython and other WSGI #
|
||||
|
||||
def startup_modpython(req=None):
|
||||
"""Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
|
||||
"""
|
||||
if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
|
||||
if req:
|
||||
if 'nullreq' in req.get_options():
|
||||
cherrypy.engine.request_class = NullRequest
|
||||
cherrypy.engine.response_class = NullResponse
|
||||
ab_opt = req.get_options().get('ab', '')
|
||||
if ab_opt:
|
||||
global AB_PATH
|
||||
AB_PATH = ab_opt
|
||||
cherrypy.engine.start()
|
||||
if cherrypy.engine.state == cherrypy._cpengine.STARTING:
|
||||
cherrypy.engine.wait()
|
||||
return 0 # apache.OK
|
||||
|
||||
|
||||
def run_modpython(use_wsgi=False):
|
||||
print('Starting mod_python...')
|
||||
pyopts = []
|
||||
|
||||
# Pass the null and ab=path options through Apache
|
||||
if '--null' in opts:
|
||||
pyopts.append(('nullreq', ''))
|
||||
|
||||
if '--ab' in opts:
|
||||
pyopts.append(('ab', opts['--ab']))
|
||||
|
||||
s = _cpmodpy.ModPythonServer
|
||||
if use_wsgi:
|
||||
pyopts.append(('wsgi.application', 'cherrypy::tree'))
|
||||
pyopts.append(
|
||||
('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython'))
|
||||
handler = 'modpython_gateway::handler'
|
||||
s = s(port=54583, opts=pyopts,
|
||||
apache_path=APACHE_PATH, handler=handler)
|
||||
else:
|
||||
pyopts.append(
|
||||
('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython'))
|
||||
s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
|
||||
|
||||
try:
|
||||
s.start()
|
||||
run()
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init()
|
||||
|
||||
longopts = ['cpmodpy', 'modpython', 'null', 'notests',
|
||||
'help', 'ab=', 'apache=']
|
||||
try:
|
||||
switches, args = getopt.getopt(sys.argv[1:], '', longopts)
|
||||
opts = dict(switches)
|
||||
except getopt.GetoptError:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
|
||||
if '--help' in opts:
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
if '--ab' in opts:
|
||||
AB_PATH = opts['--ab']
|
||||
|
||||
if '--notests' in opts:
|
||||
# Return without stopping the server, so that the pages
|
||||
# can be tested from a standard web browser.
|
||||
def run():
|
||||
port = cherrypy.server.socket_port
|
||||
print('You may now open http://127.0.0.1:%s%s/' %
|
||||
(port, SCRIPT_NAME))
|
||||
|
||||
if '--null' in opts:
|
||||
print('Using null Request object')
|
||||
else:
|
||||
def run():
|
||||
end = time.time() - start
|
||||
print('Started in %s seconds' % end)
|
||||
if '--null' in opts:
|
||||
print('\nUsing null Request object')
|
||||
try:
|
||||
try:
|
||||
run_standard_benchmarks()
|
||||
except Exception:
|
||||
print(_cperror.format_exc())
|
||||
raise
|
||||
finally:
|
||||
cherrypy.engine.exit()
|
||||
|
||||
print('Starting CherryPy app server...')
|
||||
|
||||
class NullWriter(object):
|
||||
|
||||
"""Suppresses the printing of socket errors."""
|
||||
|
||||
def write(self, data):
|
||||
pass
|
||||
sys.stderr = NullWriter()
|
||||
|
||||
start = time.time()
|
||||
|
||||
if '--cpmodpy' in opts:
|
||||
run_modpython()
|
||||
elif '--modpython' in opts:
|
||||
run_modpython(use_wsgi=True)
|
||||
else:
|
||||
if '--null' in opts:
|
||||
cherrypy.server.request_class = NullRequest
|
||||
cherrypy.server.response_class = NullResponse
|
||||
|
||||
cherrypy.engine.start_with_callback(run)
|
||||
cherrypy.engine.block()
|
||||
49
lib/cherrypy/test/checkerdemo.py
Normal file
49
lib/cherrypy/test/checkerdemo.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Demonstration app for cherrypy.checker.
|
||||
|
||||
This application is intentionally broken and badly designed.
|
||||
To demonstrate the output of the CherryPy Checker, simply execute
|
||||
this module.
|
||||
"""
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
thisdir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class Root:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
conf = {'/base': {'tools.staticdir.root': thisdir,
|
||||
# Obsolete key.
|
||||
'throw_errors': True,
|
||||
},
|
||||
# This entry should be OK.
|
||||
'/base/static': {'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static'},
|
||||
# Warn on missing folder.
|
||||
'/base/js': {'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'js'},
|
||||
# Warn on dir with an abs path even though we provide root.
|
||||
'/base/static2': {'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': '/static'},
|
||||
# Warn on dir with a relative path with no root.
|
||||
'/static3': {'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static'},
|
||||
# Warn on unknown namespace
|
||||
'/unknown': {'toobles.gzip.on': True},
|
||||
# Warn special on cherrypy.<known ns>.*
|
||||
'/cpknown': {'cherrypy.tools.encode.on': True},
|
||||
# Warn on mismatched types
|
||||
'/conftype': {'request.show_tracebacks': 14},
|
||||
# Warn on unknown tool.
|
||||
'/web': {'tools.unknown.on': True},
|
||||
# Warn on server.* in app config.
|
||||
'/app1': {'server.socket_host': '0.0.0.0'},
|
||||
# Warn on 'localhost'
|
||||
'global': {'server.socket_host': 'localhost'},
|
||||
# Warn on '[name]'
|
||||
'[/extra_brackets]': {},
|
||||
}
|
||||
cherrypy.quickstart(Root(), config=conf)
|
||||
18
lib/cherrypy/test/fastcgi.conf
Normal file
18
lib/cherrypy/test/fastcgi.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
|
||||
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
|
||||
ServerRoot /usr/lib/apache2
|
||||
User #1000
|
||||
ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log
|
||||
|
||||
DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test"
|
||||
ServerName 127.0.0.1
|
||||
Listen 8080
|
||||
LoadModule fastcgi_module modules/mod_fastcgi.so
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
Options +ExecCGI
|
||||
SetHandler fastcgi-script
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ /fastcgi.pyc [L]
|
||||
FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
|
||||
14
lib/cherrypy/test/fcgi.conf
Normal file
14
lib/cherrypy/test/fcgi.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
# Apache2 server conf file for testing CherryPy with mod_fcgid.
|
||||
|
||||
DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test"
|
||||
ServerName 127.0.0.1
|
||||
Listen 8080
|
||||
LoadModule fastcgi_module modules/mod_fastcgi.dll
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
Options ExecCGI
|
||||
SetHandler fastcgi-script
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ /fastcgi.pyc [L]
|
||||
FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
|
||||
542
lib/cherrypy/test/helper.py
Normal file
542
lib/cherrypy/test/helper.py
Normal file
@@ -0,0 +1,542 @@
|
||||
"""A library of helper functions for the CherryPy test suite."""
|
||||
|
||||
import datetime
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import portend
|
||||
import pytest
|
||||
import six
|
||||
|
||||
from cheroot.test import webtest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob
|
||||
from cherrypy.lib import httputil
|
||||
from cherrypy.lib import gctools
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
thisdir = os.path.abspath(os.path.dirname(__file__))
|
||||
serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
|
||||
|
||||
|
||||
class Supervisor(object):
|
||||
|
||||
"""Base class for modeling and controlling servers during testing."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
if k == 'port':
|
||||
setattr(self, k, int(v))
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
def log_to_stderr(msg, level):
|
||||
return sys.stderr.write(msg + os.linesep)
|
||||
|
||||
|
||||
class LocalSupervisor(Supervisor):
|
||||
|
||||
"""Base class for modeling/controlling servers which run in the same
|
||||
process.
|
||||
|
||||
When the server side runs in a different process, start/stop can dump all
|
||||
state between each test module easily. When the server side runs in the
|
||||
same process as the client, however, we have to do a bit more work to
|
||||
ensure config and mounted apps are reset between tests.
|
||||
"""
|
||||
|
||||
using_apache = False
|
||||
using_wsgi = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
cherrypy.server.httpserver = self.httpserver_class
|
||||
|
||||
# This is perhaps the wrong place for this call but this is the only
|
||||
# place that i've found so far that I KNOW is early enough to set this.
|
||||
cherrypy.config.update({'log.screen': False})
|
||||
engine = cherrypy.engine
|
||||
if hasattr(engine, 'signal_handler'):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, 'console_control_handler'):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
def start(self, modulename=None):
|
||||
"""Load and start the HTTP server."""
|
||||
if modulename:
|
||||
# Unhook httpserver so cherrypy.server.start() creates a new
|
||||
# one (with config from setup_server, if declared).
|
||||
cherrypy.server.httpserver = None
|
||||
|
||||
cherrypy.engine.start()
|
||||
|
||||
self.sync_apps()
|
||||
|
||||
def sync_apps(self):
|
||||
"""Tell the server about any apps which the setup functions mounted."""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
td = getattr(self, 'teardown', None)
|
||||
if td:
|
||||
td()
|
||||
|
||||
cherrypy.engine.exit()
|
||||
|
||||
servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
|
||||
for name, server in servers_copy:
|
||||
server.unsubscribe()
|
||||
del cherrypy.servers[name]
|
||||
|
||||
|
||||
class NativeServerSupervisor(LocalSupervisor):
|
||||
|
||||
"""Server supervisor for the builtin HTTP server."""
|
||||
|
||||
httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
|
||||
using_apache = False
|
||||
using_wsgi = False
|
||||
|
||||
def __str__(self):
|
||||
return 'Builtin HTTP Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
|
||||
class LocalWSGISupervisor(LocalSupervisor):
|
||||
|
||||
"""Server supervisor for the builtin WSGI server."""
|
||||
|
||||
httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
|
||||
using_apache = False
|
||||
using_wsgi = True
|
||||
|
||||
def __str__(self):
|
||||
return 'Builtin WSGI Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
def sync_apps(self):
|
||||
"""Hook a new WSGI app into the origin server."""
|
||||
cherrypy.server.httpserver.wsgi_app = self.get_app()
|
||||
|
||||
def get_app(self, app=None):
|
||||
"""Obtain a new (decorated) WSGI app to hook into the origin server."""
|
||||
if app is None:
|
||||
app = cherrypy.tree
|
||||
|
||||
if self.validate:
|
||||
try:
|
||||
from wsgiref import validate
|
||||
except ImportError:
|
||||
warnings.warn(
|
||||
'Error importing wsgiref. The validator will not run.')
|
||||
else:
|
||||
# wraps the app in the validator
|
||||
app = validate.validator(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def get_cpmodpy_supervisor(**options):
|
||||
from cherrypy.test import modpy
|
||||
sup = modpy.ModPythonSupervisor(**options)
|
||||
sup.template = modpy.conf_cpmodpy
|
||||
return sup
|
||||
|
||||
|
||||
def get_modpygw_supervisor(**options):
|
||||
from cherrypy.test import modpy
|
||||
sup = modpy.ModPythonSupervisor(**options)
|
||||
sup.template = modpy.conf_modpython_gateway
|
||||
sup.using_wsgi = True
|
||||
return sup
|
||||
|
||||
|
||||
def get_modwsgi_supervisor(**options):
|
||||
from cherrypy.test import modwsgi
|
||||
return modwsgi.ModWSGISupervisor(**options)
|
||||
|
||||
|
||||
def get_modfcgid_supervisor(**options):
|
||||
from cherrypy.test import modfcgid
|
||||
return modfcgid.ModFCGISupervisor(**options)
|
||||
|
||||
|
||||
def get_modfastcgi_supervisor(**options):
|
||||
from cherrypy.test import modfastcgi
|
||||
return modfastcgi.ModFCGISupervisor(**options)
|
||||
|
||||
|
||||
def get_wsgi_u_supervisor(**options):
|
||||
cherrypy.server.wsgi_version = ('u', 0)
|
||||
return LocalWSGISupervisor(**options)
|
||||
|
||||
|
||||
class CPWebCase(webtest.WebCase):
|
||||
|
||||
script_name = ''
|
||||
scheme = 'http'
|
||||
|
||||
available_servers = {'wsgi': LocalWSGISupervisor,
|
||||
'wsgi_u': get_wsgi_u_supervisor,
|
||||
'native': NativeServerSupervisor,
|
||||
'cpmodpy': get_cpmodpy_supervisor,
|
||||
'modpygw': get_modpygw_supervisor,
|
||||
'modwsgi': get_modwsgi_supervisor,
|
||||
'modfcgid': get_modfcgid_supervisor,
|
||||
'modfastcgi': get_modfastcgi_supervisor,
|
||||
}
|
||||
default_server = 'wsgi'
|
||||
|
||||
@classmethod
|
||||
def _setup_server(cls, supervisor, conf):
|
||||
v = sys.version.split()[0]
|
||||
log.info('Python version used to run this test script: %s' % v)
|
||||
log.info('CherryPy version: %s' % cherrypy.__version__)
|
||||
if supervisor.scheme == 'https':
|
||||
ssl = ' (ssl)'
|
||||
else:
|
||||
ssl = ''
|
||||
log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl))
|
||||
log.info('PID: %s' % os.getpid())
|
||||
|
||||
cherrypy.server.using_apache = supervisor.using_apache
|
||||
cherrypy.server.using_wsgi = supervisor.using_wsgi
|
||||
|
||||
if sys.platform[:4] == 'java':
|
||||
cherrypy.config.update({'server.nodelay': False})
|
||||
|
||||
if isinstance(conf, text_or_bytes):
|
||||
parser = cherrypy.lib.reprconf.Parser()
|
||||
conf = parser.dict_from_file(conf).get('global', {})
|
||||
else:
|
||||
conf = conf or {}
|
||||
baseconf = conf.copy()
|
||||
baseconf.update({'server.socket_host': supervisor.host,
|
||||
'server.socket_port': supervisor.port,
|
||||
'server.protocol_version': supervisor.protocol,
|
||||
'environment': 'test_suite',
|
||||
})
|
||||
if supervisor.scheme == 'https':
|
||||
# baseconf['server.ssl_module'] = 'builtin'
|
||||
baseconf['server.ssl_certificate'] = serverpem
|
||||
baseconf['server.ssl_private_key'] = serverpem
|
||||
|
||||
# helper must be imported lazily so the coverage tool
|
||||
# can run against module-level statements within cherrypy.
|
||||
# Also, we have to do "from cherrypy.test import helper",
|
||||
# exactly like each test module does, because a relative import
|
||||
# would stick a second instance of webtest in sys.modules,
|
||||
# and we wouldn't be able to globally override the port anymore.
|
||||
if supervisor.scheme == 'https':
|
||||
webtest.WebCase.HTTP_CONN = HTTPSConnection
|
||||
return baseconf
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
''
|
||||
# Creates a server
|
||||
conf = {
|
||||
'scheme': 'http',
|
||||
'protocol': 'HTTP/1.1',
|
||||
'port': 54583,
|
||||
'host': '127.0.0.1',
|
||||
'validate': False,
|
||||
'server': 'wsgi',
|
||||
}
|
||||
supervisor_factory = cls.available_servers.get(
|
||||
conf.get('server', 'wsgi'))
|
||||
if supervisor_factory is None:
|
||||
raise RuntimeError('Unknown server in config: %s' % conf['server'])
|
||||
supervisor = supervisor_factory(**conf)
|
||||
|
||||
# Copied from "run_test_suite"
|
||||
cherrypy.config.reset()
|
||||
baseconf = cls._setup_server(supervisor, conf)
|
||||
cherrypy.config.update(baseconf)
|
||||
setup_client()
|
||||
|
||||
if hasattr(cls, 'setup_server'):
|
||||
# Clear the cherrypy tree and clear the wsgi server so that
|
||||
# it can be updated with the new root
|
||||
cherrypy.tree = cherrypy._cptree.Tree()
|
||||
cherrypy.server.httpserver = None
|
||||
cls.setup_server()
|
||||
# Add a resource for verifying there are no refleaks
|
||||
# to *every* test class.
|
||||
cherrypy.tree.mount(gctools.GCRoot(), '/gc')
|
||||
cls.do_gc_test = True
|
||||
supervisor.start(cls.__module__)
|
||||
|
||||
cls.supervisor = supervisor
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
''
|
||||
if hasattr(cls, 'setup_server'):
|
||||
cls.supervisor.stop()
|
||||
|
||||
do_gc_test = False
|
||||
|
||||
def test_gc(self):
|
||||
if not self.do_gc_test:
|
||||
return
|
||||
|
||||
self.getPage('/gc/stats')
|
||||
try:
|
||||
self.assertBody('Statistics:')
|
||||
except Exception:
|
||||
'Failures occur intermittently. See #1420'
|
||||
|
||||
def prefix(self):
|
||||
return self.script_name.rstrip('/')
|
||||
|
||||
def base(self):
|
||||
if ((self.scheme == 'http' and self.PORT == 80) or
|
||||
(self.scheme == 'https' and self.PORT == 443)):
|
||||
port = ''
|
||||
else:
|
||||
port = ':%s' % self.PORT
|
||||
|
||||
return '%s://%s%s%s' % (self.scheme, self.HOST, port,
|
||||
self.script_name.rstrip('/'))
|
||||
|
||||
def exit(self):
|
||||
sys.exit()
|
||||
|
||||
def getPage(self, url, headers=None, method='GET', body=None,
|
||||
protocol=None, raise_subcls=None):
|
||||
"""Open the url. Return status, headers, body.
|
||||
|
||||
`raise_subcls` must be a tuple with the exceptions classes
|
||||
or a single exception class that are not going to be considered
|
||||
a socket.error regardless that they were are subclass of a
|
||||
socket.error and therefore not considered for a connection retry.
|
||||
"""
|
||||
if self.script_name:
|
||||
url = httputil.urljoin(self.script_name, url)
|
||||
return webtest.WebCase.getPage(self, url, headers, method, body,
|
||||
protocol, raise_subcls)
|
||||
|
||||
def skip(self, msg='skipped '):
|
||||
pytest.skip(msg)
|
||||
|
||||
def assertErrorPage(self, status, message=None, pattern=''):
|
||||
"""Compare the response body with a built in error page.
|
||||
|
||||
The function will optionally look for the regexp pattern,
|
||||
within the exception embedded in the error page."""
|
||||
|
||||
# This will never contain a traceback
|
||||
page = cherrypy._cperror.get_error_page(status, message=message)
|
||||
|
||||
# First, test the response body without checking the traceback.
|
||||
# Stick a match-all group (.*) in to grab the traceback.
|
||||
def esc(text):
|
||||
return re.escape(ntob(text))
|
||||
epage = re.escape(page)
|
||||
epage = epage.replace(
|
||||
esc('<pre id="traceback"></pre>'),
|
||||
esc('<pre id="traceback">') + b'(.*)' + esc('</pre>'))
|
||||
m = re.match(epage, self.body, re.DOTALL)
|
||||
if not m:
|
||||
self._handlewebError(
|
||||
'Error page does not match; expected:\n' + page)
|
||||
return
|
||||
|
||||
# Now test the pattern against the traceback
|
||||
if pattern is None:
|
||||
# Special-case None to mean that there should be *no* traceback.
|
||||
if m and m.group(1):
|
||||
self._handlewebError('Error page contains traceback')
|
||||
else:
|
||||
if (m is None) or (
|
||||
not re.search(ntob(re.escape(pattern), self.encoding),
|
||||
m.group(1))):
|
||||
msg = 'Error page does not contain %s in traceback'
|
||||
self._handlewebError(msg % repr(pattern))
|
||||
|
||||
date_tolerance = 2
|
||||
|
||||
def assertEqualDates(self, dt1, dt2, seconds=None):
|
||||
"""Assert abs(dt1 - dt2) is within Y seconds."""
|
||||
if seconds is None:
|
||||
seconds = self.date_tolerance
|
||||
|
||||
if dt1 > dt2:
|
||||
diff = dt1 - dt2
|
||||
else:
|
||||
diff = dt2 - dt1
|
||||
if not diff < datetime.timedelta(seconds=seconds):
|
||||
raise AssertionError('%r and %r are not within %r seconds.' %
|
||||
(dt1, dt2, seconds))
|
||||
|
||||
|
||||
def _test_method_sorter(_, x, y):
|
||||
"""Monkeypatch the test sorter to always run test_gc last in each suite."""
|
||||
if x == 'test_gc':
|
||||
return 1
|
||||
if y == 'test_gc':
|
||||
return -1
|
||||
if x > y:
|
||||
return 1
|
||||
if x < y:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
|
||||
|
||||
|
||||
def setup_client():
|
||||
"""Set up the WebCase classes to match the server's socket settings."""
|
||||
webtest.WebCase.PORT = cherrypy.server.socket_port
|
||||
webtest.WebCase.HOST = cherrypy.server.socket_host
|
||||
if cherrypy.server.ssl_certificate:
|
||||
CPWebCase.scheme = 'https'
|
||||
|
||||
# --------------------------- Spawning helpers --------------------------- #
|
||||
|
||||
|
||||
class CPProcess(object):
|
||||
|
||||
pid_file = os.path.join(thisdir, 'test.pid')
|
||||
config_file = os.path.join(thisdir, 'test.conf')
|
||||
config_template = """[global]
|
||||
server.socket_host: '%(host)s'
|
||||
server.socket_port: %(port)s
|
||||
checker.on: False
|
||||
log.screen: False
|
||||
log.error_file: r'%(error_log)s'
|
||||
log.access_file: r'%(access_log)s'
|
||||
%(ssl)s
|
||||
%(extra)s
|
||||
"""
|
||||
error_log = os.path.join(thisdir, 'test.error.log')
|
||||
access_log = os.path.join(thisdir, 'test.access.log')
|
||||
|
||||
def __init__(self, wait=False, daemonize=False, ssl=False,
|
||||
socket_host=None, socket_port=None):
|
||||
self.wait = wait
|
||||
self.daemonize = daemonize
|
||||
self.ssl = ssl
|
||||
self.host = socket_host or cherrypy.server.socket_host
|
||||
self.port = socket_port or cherrypy.server.socket_port
|
||||
|
||||
def write_conf(self, extra=''):
|
||||
if self.ssl:
|
||||
serverpem = os.path.join(thisdir, 'test.pem')
|
||||
ssl = """
|
||||
server.ssl_certificate: r'%s'
|
||||
server.ssl_private_key: r'%s'
|
||||
""" % (serverpem, serverpem)
|
||||
else:
|
||||
ssl = ''
|
||||
|
||||
conf = self.config_template % {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'error_log': self.error_log,
|
||||
'access_log': self.access_log,
|
||||
'ssl': ssl,
|
||||
'extra': extra,
|
||||
}
|
||||
with io.open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
f.write(six.text_type(conf))
|
||||
|
||||
def start(self, imports=None):
|
||||
"""Start cherryd in a subprocess."""
|
||||
portend.free(self.host, self.port, timeout=1)
|
||||
|
||||
args = [
|
||||
'-m',
|
||||
'cherrypy',
|
||||
'-c', self.config_file,
|
||||
'-p', self.pid_file,
|
||||
]
|
||||
r"""
|
||||
Command for running cherryd server with autoreload enabled
|
||||
|
||||
Using
|
||||
|
||||
```
|
||||
['-c',
|
||||
"__requires__ = 'CherryPy'; \
|
||||
import pkg_resources, re, sys; \
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
|
||||
sys.exit(\
|
||||
pkg_resources.load_entry_point(\
|
||||
'CherryPy', 'console_scripts', 'cherryd')())"]
|
||||
```
|
||||
|
||||
doesn't work as it's impossible to reconstruct the `-c`'s contents.
|
||||
Ref: https://github.com/cherrypy/cherrypy/issues/1545
|
||||
"""
|
||||
|
||||
if not isinstance(imports, (list, tuple)):
|
||||
imports = [imports]
|
||||
for i in imports:
|
||||
if i:
|
||||
args.append('-i')
|
||||
args.append(i)
|
||||
|
||||
if self.daemonize:
|
||||
args.append('-d')
|
||||
|
||||
env = os.environ.copy()
|
||||
# Make sure we import the cherrypy package in which this module is
|
||||
# defined.
|
||||
grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
|
||||
if env.get('PYTHONPATH', ''):
|
||||
env['PYTHONPATH'] = os.pathsep.join(
|
||||
(grandparentdir, env['PYTHONPATH']))
|
||||
else:
|
||||
env['PYTHONPATH'] = grandparentdir
|
||||
self._proc = subprocess.Popen([sys.executable] + args, env=env)
|
||||
if self.wait:
|
||||
self.exit_code = self._proc.wait()
|
||||
else:
|
||||
portend.occupied(self.host, self.port, timeout=5)
|
||||
|
||||
# Give the engine a wee bit more time to finish STARTING
|
||||
if self.daemonize:
|
||||
time.sleep(2)
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
def get_pid(self):
|
||||
if self.daemonize:
|
||||
return int(open(self.pid_file, 'rb').read())
|
||||
return self._proc.pid
|
||||
|
||||
def join(self):
|
||||
"""Wait for the process to exit."""
|
||||
if self.daemonize:
|
||||
return self._join_daemon()
|
||||
self._proc.wait()
|
||||
|
||||
def _join_daemon(self):
|
||||
try:
|
||||
try:
|
||||
# Mac, UNIX
|
||||
os.wait()
|
||||
except AttributeError:
|
||||
# Windows
|
||||
try:
|
||||
pid = self.get_pid()
|
||||
except IOError:
|
||||
# Assume the subprocess deleted the pidfile on shutdown.
|
||||
pass
|
||||
else:
|
||||
os.waitpid(pid, 0)
|
||||
except OSError:
|
||||
x = sys.exc_info()[1]
|
||||
if x.args != (10, 'No child processes'):
|
||||
raise
|
||||
228
lib/cherrypy/test/logtest.py
Normal file
228
lib/cherrypy/test/logtest.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""logtest, a unittest.TestCase helper for testing log output."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from uuid import UUID
|
||||
|
||||
import six
|
||||
|
||||
from cherrypy._cpcompat import text_or_bytes, ntob
|
||||
|
||||
|
||||
try:
|
||||
# On Windows, msvcrt.getch reads a single char without output.
|
||||
import msvcrt
|
||||
|
||||
def getchar():
|
||||
return msvcrt.getch()
|
||||
except ImportError:
|
||||
# Unix getchr
|
||||
import tty
|
||||
import termios
|
||||
|
||||
def getchar():
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
|
||||
class LogCase(object):
|
||||
|
||||
"""unittest.TestCase mixin for testing log messages.
|
||||
|
||||
logfile: a filename for the desired log. Yes, I know modes are evil,
|
||||
but it makes the test functions so much cleaner to set this once.
|
||||
|
||||
lastmarker: the last marker in the log. This can be used to search for
|
||||
messages since the last marker.
|
||||
|
||||
markerPrefix: a string with which to prefix log markers. This should be
|
||||
unique enough from normal log output to use for marker identification.
|
||||
"""
|
||||
|
||||
logfile = None
|
||||
lastmarker = None
|
||||
markerPrefix = b'test suite marker: '
|
||||
|
||||
def _handleLogError(self, msg, data, marker, pattern):
|
||||
print('')
|
||||
print(' ERROR: %s' % msg)
|
||||
|
||||
if not self.interactive:
|
||||
raise self.failureException(msg)
|
||||
|
||||
p = (' Show: '
|
||||
'[L]og [M]arker [P]attern; '
|
||||
'[I]gnore, [R]aise, or sys.e[X]it >> ')
|
||||
sys.stdout.write(p + ' ')
|
||||
# ARGH
|
||||
sys.stdout.flush()
|
||||
while True:
|
||||
i = getchar().upper()
|
||||
if i not in 'MPLIRX':
|
||||
continue
|
||||
print(i.upper()) # Also prints new line
|
||||
if i == 'L':
|
||||
for x, line in enumerate(data):
|
||||
if (x + 1) % self.console_height == 0:
|
||||
# The \r and comma should make the next line overwrite
|
||||
sys.stdout.write('<-- More -->\r ')
|
||||
m = getchar().lower()
|
||||
# Erase our "More" prompt
|
||||
sys.stdout.write(' \r ')
|
||||
if m == 'q':
|
||||
break
|
||||
print(line.rstrip())
|
||||
elif i == 'M':
|
||||
print(repr(marker or self.lastmarker))
|
||||
elif i == 'P':
|
||||
print(repr(pattern))
|
||||
elif i == 'I':
|
||||
# return without raising the normal exception
|
||||
return
|
||||
elif i == 'R':
|
||||
raise self.failureException(msg)
|
||||
elif i == 'X':
|
||||
self.exit()
|
||||
sys.stdout.write(p + ' ')
|
||||
|
||||
def exit(self):
|
||||
sys.exit()
|
||||
|
||||
def emptyLog(self):
|
||||
"""Overwrite self.logfile with 0 bytes."""
|
||||
open(self.logfile, 'wb').write('')
|
||||
|
||||
def markLog(self, key=None):
|
||||
"""Insert a marker line into the log and set self.lastmarker."""
|
||||
if key is None:
|
||||
key = str(time.time())
|
||||
self.lastmarker = key
|
||||
|
||||
open(self.logfile, 'ab+').write(
|
||||
ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
|
||||
|
||||
def _read_marked_region(self, marker=None):
|
||||
"""Return lines from self.logfile in the marked region.
|
||||
|
||||
If marker is None, self.lastmarker is used. If the log hasn't
|
||||
been marked (using self.markLog), the entire log will be returned.
|
||||
"""
|
||||
# Give the logger time to finish writing?
|
||||
# time.sleep(0.5)
|
||||
|
||||
logfile = self.logfile
|
||||
marker = marker or self.lastmarker
|
||||
if marker is None:
|
||||
return open(logfile, 'rb').readlines()
|
||||
|
||||
if isinstance(marker, six.text_type):
|
||||
marker = marker.encode('utf-8')
|
||||
data = []
|
||||
in_region = False
|
||||
for line in open(logfile, 'rb'):
|
||||
if in_region:
|
||||
if line.startswith(self.markerPrefix) and marker not in line:
|
||||
break
|
||||
else:
|
||||
data.append(line)
|
||||
elif marker in line:
|
||||
in_region = True
|
||||
return data
|
||||
|
||||
def assertInLog(self, line, marker=None):
|
||||
"""Fail if the given (partial) line is not in the log.
|
||||
|
||||
The log will be searched from the given marker to the next marker.
|
||||
If marker is None, self.lastmarker is used. If the log hasn't
|
||||
been marked (using self.markLog), the entire log will be searched.
|
||||
"""
|
||||
data = self._read_marked_region(marker)
|
||||
for logline in data:
|
||||
if line in logline:
|
||||
return
|
||||
msg = '%r not found in log' % line
|
||||
self._handleLogError(msg, data, marker, line)
|
||||
|
||||
def assertNotInLog(self, line, marker=None):
|
||||
"""Fail if the given (partial) line is in the log.
|
||||
|
||||
The log will be searched from the given marker to the next marker.
|
||||
If marker is None, self.lastmarker is used. If the log hasn't
|
||||
been marked (using self.markLog), the entire log will be searched.
|
||||
"""
|
||||
data = self._read_marked_region(marker)
|
||||
for logline in data:
|
||||
if line in logline:
|
||||
msg = '%r found in log' % line
|
||||
self._handleLogError(msg, data, marker, line)
|
||||
|
||||
def assertValidUUIDv4(self, marker=None):
|
||||
"""Fail if the given UUIDv4 is not valid.
|
||||
|
||||
The log will be searched from the given marker to the next marker.
|
||||
If marker is None, self.lastmarker is used. If the log hasn't
|
||||
been marked (using self.markLog), the entire log will be searched.
|
||||
"""
|
||||
data = self._read_marked_region(marker)
|
||||
data = [
|
||||
chunk.decode('utf-8').rstrip('\n').rstrip('\r')
|
||||
for chunk in data
|
||||
]
|
||||
for log_chunk in data:
|
||||
try:
|
||||
uuid_log = data[-1]
|
||||
uuid_obj = UUID(uuid_log, version=4)
|
||||
except (TypeError, ValueError):
|
||||
pass # it might be in other chunk
|
||||
else:
|
||||
if str(uuid_obj) == uuid_log:
|
||||
return
|
||||
msg = '%r is not a valid UUIDv4' % uuid_log
|
||||
self._handleLogError(msg, data, marker, log_chunk)
|
||||
|
||||
msg = 'UUIDv4 not found in log'
|
||||
self._handleLogError(msg, data, marker, log_chunk)
|
||||
|
||||
def assertLog(self, sliceargs, lines, marker=None):
|
||||
"""Fail if log.readlines()[sliceargs] is not contained in 'lines'.
|
||||
|
||||
The log will be searched from the given marker to the next marker.
|
||||
If marker is None, self.lastmarker is used. If the log hasn't
|
||||
been marked (using self.markLog), the entire log will be searched.
|
||||
"""
|
||||
data = self._read_marked_region(marker)
|
||||
if isinstance(sliceargs, int):
|
||||
# Single arg. Use __getitem__ and allow lines to be str or list.
|
||||
if isinstance(lines, (tuple, list)):
|
||||
lines = lines[0]
|
||||
if isinstance(lines, six.text_type):
|
||||
lines = lines.encode('utf-8')
|
||||
if lines not in data[sliceargs]:
|
||||
msg = '%r not found on log line %r' % (lines, sliceargs)
|
||||
self._handleLogError(
|
||||
msg,
|
||||
[data[sliceargs], '--EXTRA CONTEXT--'] + data[
|
||||
sliceargs + 1:sliceargs + 6],
|
||||
marker,
|
||||
lines)
|
||||
else:
|
||||
# Multiple args. Use __getslice__ and require lines to be list.
|
||||
if isinstance(lines, tuple):
|
||||
lines = list(lines)
|
||||
elif isinstance(lines, text_or_bytes):
|
||||
raise TypeError("The 'lines' arg must be a list when "
|
||||
"'sliceargs' is a tuple.")
|
||||
|
||||
start, stop = sliceargs
|
||||
for line, logline in zip(lines, data[start:stop]):
|
||||
if isinstance(line, six.text_type):
|
||||
line = line.encode('utf-8')
|
||||
if line not in logline:
|
||||
msg = '%r not found in log' % line
|
||||
self._handleLogError(msg, data[start:stop], marker, line)
|
||||
136
lib/cherrypy/test/modfastcgi.py
Normal file
136
lib/cherrypy/test/modfastcgi.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
|
||||
|
||||
To autostart fastcgi, the "apache" executable or script must be
|
||||
on your system path, or you must override the global APACHE_PATH.
|
||||
On some platforms, "apache" may be called "apachectl", "apache2ctl",
|
||||
or "httpd"--create a symlink to them if needed.
|
||||
|
||||
You'll also need the WSGIServer from flup.servers.
|
||||
See http://projects.amor.org/misc/wiki/ModPythonGateway
|
||||
|
||||
|
||||
KNOWN BUGS
|
||||
==========
|
||||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
4. Apache replaces status "reason phrases" automatically. For example,
|
||||
CherryPy may set "304 Not modified" but Apache will write out
|
||||
"304 Not Modified" (capital "M").
|
||||
5. Apache does not allow custom error codes as per the spec.
|
||||
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
||||
Request-URI too early.
|
||||
7. mod_python will not read request bodies which use the "chunked"
|
||||
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
||||
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
||||
mod_python's requestobject.c).
|
||||
8. Apache will output a "Content-Length: 0" response header even if there's
|
||||
no response entity body. This isn't really a bug; it just differs from
|
||||
the CherryPy default.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.process import servers
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(r'(not recognized|No such file|not found)', firstline,
|
||||
re.IGNORECASE)):
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
APACHE_PATH = 'apache2ctl'
|
||||
CONF_PATH = 'fastcgi.conf'
|
||||
|
||||
conf_fastcgi = """
|
||||
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
|
||||
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
|
||||
ServerRoot /usr/lib/apache2
|
||||
User #1000
|
||||
ErrorLog %(root)s/mod_fastcgi.error.log
|
||||
|
||||
DocumentRoot "%(root)s"
|
||||
ServerName 127.0.0.1
|
||||
Listen %(port)s
|
||||
LoadModule fastcgi_module modules/mod_fastcgi.so
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
Options +ExecCGI
|
||||
SetHandler fastcgi-script
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ /fastcgi.pyc [L]
|
||||
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
|
||||
"""
|
||||
|
||||
|
||||
def erase_script_name(environ, start_response):
|
||||
environ['SCRIPT_NAME'] = ''
|
||||
return cherrypy.tree(environ, start_response)
|
||||
|
||||
|
||||
class ModFCGISupervisor(helper.LocalWSGISupervisor):
|
||||
|
||||
httpserver_class = 'cherrypy.process.servers.FlupFCGIServer'
|
||||
using_apache = True
|
||||
using_wsgi = True
|
||||
template = conf_fastcgi
|
||||
|
||||
def __str__(self):
|
||||
return 'FCGI Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
def start(self, modulename):
|
||||
cherrypy.server.httpserver = servers.FlupFCGIServer(
|
||||
application=erase_script_name, bindAddress=('127.0.0.1', 4000))
|
||||
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
|
||||
cherrypy.server.socket_port = 4000
|
||||
# For FCGI, we both start apache...
|
||||
self.start_apache()
|
||||
# ...and our local server
|
||||
cherrypy.engine.start()
|
||||
self.sync_apps()
|
||||
|
||||
def start_apache(self):
|
||||
fcgiconf = CONF_PATH
|
||||
if not os.path.isabs(fcgiconf):
|
||||
fcgiconf = os.path.join(curdir, fcgiconf)
|
||||
|
||||
# Write the Apache conf file.
|
||||
f = open(fcgiconf, 'wb')
|
||||
try:
|
||||
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
|
||||
output = self.template % {'port': self.port, 'root': curdir,
|
||||
'server': server}
|
||||
output = output.replace('\r\n', '\n')
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
|
||||
if result:
|
||||
print(result)
|
||||
|
||||
def stop(self):
|
||||
"""Gracefully shutdown a server that is serving forever."""
|
||||
read_process(APACHE_PATH, '-k stop')
|
||||
helper.LocalWSGISupervisor.stop(self)
|
||||
|
||||
def sync_apps(self):
|
||||
cherrypy.server.httpserver.fcgiserver.application = self.get_app(
|
||||
erase_script_name)
|
||||
124
lib/cherrypy/test/modfcgid.py
Normal file
124
lib/cherrypy/test/modfcgid.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
|
||||
|
||||
To autostart fcgid, the "apache" executable or script must be
|
||||
on your system path, or you must override the global APACHE_PATH.
|
||||
On some platforms, "apache" may be called "apachectl", "apache2ctl",
|
||||
or "httpd"--create a symlink to them if needed.
|
||||
|
||||
You'll also need the WSGIServer from flup.servers.
|
||||
See http://projects.amor.org/misc/wiki/ModPythonGateway
|
||||
|
||||
|
||||
KNOWN BUGS
|
||||
==========
|
||||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
4. Apache replaces status "reason phrases" automatically. For example,
|
||||
CherryPy may set "304 Not modified" but Apache will write out
|
||||
"304 Not Modified" (capital "M").
|
||||
5. Apache does not allow custom error codes as per the spec.
|
||||
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
||||
Request-URI too early.
|
||||
7. mod_python will not read request bodies which use the "chunked"
|
||||
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
||||
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
||||
mod_python's requestobject.c).
|
||||
8. Apache will output a "Content-Length: 0" response header even if there's
|
||||
no response entity body. This isn't really a bug; it just differs from
|
||||
the CherryPy default.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
from cherrypy.process import servers
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(r'(not recognized|No such file|not found)', firstline,
|
||||
re.IGNORECASE)):
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
APACHE_PATH = 'httpd'
|
||||
CONF_PATH = 'fcgi.conf'
|
||||
|
||||
conf_fcgid = """
|
||||
# Apache2 server conf file for testing CherryPy with mod_fcgid.
|
||||
|
||||
DocumentRoot "%(root)s"
|
||||
ServerName 127.0.0.1
|
||||
Listen %(port)s
|
||||
LoadModule fastcgi_module modules/mod_fastcgi.dll
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
Options ExecCGI
|
||||
SetHandler fastcgi-script
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ /fastcgi.pyc [L]
|
||||
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
|
||||
"""
|
||||
|
||||
|
||||
class ModFCGISupervisor(helper.LocalSupervisor):
|
||||
|
||||
using_apache = True
|
||||
using_wsgi = True
|
||||
template = conf_fcgid
|
||||
|
||||
def __str__(self):
|
||||
return 'FCGI Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
def start(self, modulename):
|
||||
cherrypy.server.httpserver = servers.FlupFCGIServer(
|
||||
application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
|
||||
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
|
||||
# For FCGI, we both start apache...
|
||||
self.start_apache()
|
||||
# ...and our local server
|
||||
helper.LocalServer.start(self, modulename)
|
||||
|
||||
def start_apache(self):
|
||||
fcgiconf = CONF_PATH
|
||||
if not os.path.isabs(fcgiconf):
|
||||
fcgiconf = os.path.join(curdir, fcgiconf)
|
||||
|
||||
# Write the Apache conf file.
|
||||
f = open(fcgiconf, 'wb')
|
||||
try:
|
||||
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
|
||||
output = self.template % {'port': self.port, 'root': curdir,
|
||||
'server': server}
|
||||
output = ntob(output.replace('\r\n', '\n'))
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
|
||||
if result:
|
||||
print(result)
|
||||
|
||||
def stop(self):
|
||||
"""Gracefully shutdown a server that is serving forever."""
|
||||
read_process(APACHE_PATH, '-k stop')
|
||||
helper.LocalServer.stop(self)
|
||||
|
||||
def sync_apps(self):
|
||||
cherrypy.server.httpserver.fcgiserver.application = self.get_app()
|
||||
164
lib/cherrypy/test/modpy.py
Normal file
164
lib/cherrypy/test/modpy.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
|
||||
|
||||
To autostart modpython, the "apache" executable or script must be
|
||||
on your system path, or you must override the global APACHE_PATH.
|
||||
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
|
||||
create a symlink to them if needed.
|
||||
|
||||
If you wish to test the WSGI interface instead of our _cpmodpy interface,
|
||||
you also need the 'modpython_gateway' module at:
|
||||
http://projects.amor.org/misc/wiki/ModPythonGateway
|
||||
|
||||
|
||||
KNOWN BUGS
|
||||
==========
|
||||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
4. Apache replaces status "reason phrases" automatically. For example,
|
||||
CherryPy may set "304 Not modified" but Apache will write out
|
||||
"304 Not Modified" (capital "M").
|
||||
5. Apache does not allow custom error codes as per the spec.
|
||||
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
||||
Request-URI too early.
|
||||
7. mod_python will not read request bodies which use the "chunked"
|
||||
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
||||
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
||||
mod_python's requestobject.c).
|
||||
8. Apache will output a "Content-Length: 0" response header even if there's
|
||||
no response entity body. This isn't really a bug; it just differs from
|
||||
the CherryPy default.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(r'(not recognized|No such file|not found)', firstline,
|
||||
re.IGNORECASE)):
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
APACHE_PATH = 'httpd'
|
||||
CONF_PATH = 'test_mp.conf'
|
||||
|
||||
conf_modpython_gateway = """
|
||||
# Apache2 server conf file for testing CherryPy with modpython_gateway.
|
||||
|
||||
ServerName 127.0.0.1
|
||||
DocumentRoot "/"
|
||||
Listen %(port)s
|
||||
LoadModule python_module modules/mod_python.so
|
||||
|
||||
SetHandler python-program
|
||||
PythonFixupHandler cherrypy.test.modpy::wsgisetup
|
||||
PythonOption testmod %(modulename)s
|
||||
PythonHandler modpython_gateway::handler
|
||||
PythonOption wsgi.application cherrypy::tree
|
||||
PythonOption socket_host %(host)s
|
||||
PythonDebug On
|
||||
"""
|
||||
|
||||
conf_cpmodpy = """
|
||||
# Apache2 server conf file for testing CherryPy with _cpmodpy.
|
||||
|
||||
ServerName 127.0.0.1
|
||||
DocumentRoot "/"
|
||||
Listen %(port)s
|
||||
LoadModule python_module modules/mod_python.so
|
||||
|
||||
SetHandler python-program
|
||||
PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
|
||||
PythonOption socket_host %(host)s
|
||||
PythonDebug On
|
||||
"""
|
||||
|
||||
|
||||
class ModPythonSupervisor(helper.Supervisor):
|
||||
|
||||
using_apache = True
|
||||
using_wsgi = False
|
||||
template = None
|
||||
|
||||
def __str__(self):
|
||||
return 'ModPython Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
def start(self, modulename):
|
||||
mpconf = CONF_PATH
|
||||
if not os.path.isabs(mpconf):
|
||||
mpconf = os.path.join(curdir, mpconf)
|
||||
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
f.write(self.template %
|
||||
{'port': self.port, 'modulename': modulename,
|
||||
'host': self.host})
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
|
||||
if result:
|
||||
print(result)
|
||||
|
||||
def stop(self):
|
||||
"""Gracefully shutdown a server that is serving forever."""
|
||||
read_process(APACHE_PATH, '-k stop')
|
||||
|
||||
|
||||
loaded = False
|
||||
|
||||
|
||||
def wsgisetup(req):
|
||||
global loaded
|
||||
if not loaded:
|
||||
loaded = True
|
||||
options = req.get_options()
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.error_file': os.path.join(curdir, 'test.log'),
|
||||
'environment': 'test_suite',
|
||||
'server.socket_host': options['socket_host'],
|
||||
})
|
||||
|
||||
modname = options['testmod']
|
||||
mod = __import__(modname, globals(), locals(), [''])
|
||||
mod.setup_server()
|
||||
|
||||
cherrypy.server.unsubscribe()
|
||||
cherrypy.engine.start()
|
||||
from mod_python import apache
|
||||
return apache.OK
|
||||
|
||||
|
||||
def cpmodpysetup(req):
|
||||
global loaded
|
||||
if not loaded:
|
||||
loaded = True
|
||||
options = req.get_options()
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.error_file': os.path.join(curdir, 'test.log'),
|
||||
'environment': 'test_suite',
|
||||
'server.socket_host': options['socket_host'],
|
||||
})
|
||||
from mod_python import apache
|
||||
return apache.OK
|
||||
154
lib/cherrypy/test/modwsgi.py
Normal file
154
lib/cherrypy/test/modwsgi.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
|
||||
|
||||
To autostart modwsgi, the "apache" executable or script must be
|
||||
on your system path, or you must override the global APACHE_PATH.
|
||||
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
|
||||
create a symlink to them if needed.
|
||||
|
||||
|
||||
KNOWN BUGS
|
||||
==========
|
||||
|
||||
##1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
## output is then truncated again by Apache. See test_core.testRanges.
|
||||
## This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
##4. Apache replaces status "reason phrases" automatically. For example,
|
||||
## CherryPy may set "304 Not modified" but Apache will write out
|
||||
## "304 Not Modified" (capital "M").
|
||||
##5. Apache does not allow custom error codes as per the spec.
|
||||
##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
||||
## Request-URI too early.
|
||||
7. mod_wsgi will not read request bodies which use the "chunked"
|
||||
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
||||
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
||||
mod_python's requestobject.c).
|
||||
8. When responding with 204 No Content, mod_wsgi adds a Content-Length
|
||||
header for you.
|
||||
9. When an error is raised, mod_wsgi has no facility for printing a
|
||||
traceback as the response content (it's sent to the Apache log instead).
|
||||
10. Startup and shutdown of Apache when running mod_wsgi seems slow.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import portend
|
||||
|
||||
from cheroot.test import webtest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(r'(not recognized|No such file|not found)', firstline,
|
||||
re.IGNORECASE)):
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
if sys.platform == 'win32':
|
||||
APACHE_PATH = 'httpd'
|
||||
else:
|
||||
APACHE_PATH = 'apache'
|
||||
|
||||
CONF_PATH = 'test_mw.conf'
|
||||
|
||||
conf_modwsgi = r"""
|
||||
# Apache2 server conf file for testing CherryPy with modpython_gateway.
|
||||
|
||||
ServerName 127.0.0.1
|
||||
DocumentRoot "/"
|
||||
Listen %(port)s
|
||||
|
||||
AllowEncodedSlashes On
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
RewriteEngine on
|
||||
RewriteMap escaping int:escape
|
||||
|
||||
LoadModule log_config_module modules/mod_log_config.so
|
||||
LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
|
||||
CustomLog "%(curdir)s/apache.access.log" combined
|
||||
ErrorLog "%(curdir)s/apache.error.log"
|
||||
LogLevel debug
|
||||
|
||||
LoadModule wsgi_module modules/mod_wsgi.so
|
||||
LoadModule env_module modules/mod_env.so
|
||||
|
||||
WSGIScriptAlias / "%(curdir)s/modwsgi.py"
|
||||
SetEnv testmod %(testmod)s
|
||||
""" # noqa E501
|
||||
|
||||
|
||||
class ModWSGISupervisor(helper.Supervisor):
|
||||
|
||||
"""Server Controller for ModWSGI and CherryPy."""
|
||||
|
||||
using_apache = True
|
||||
using_wsgi = True
|
||||
template = conf_modwsgi
|
||||
|
||||
def __str__(self):
|
||||
return 'ModWSGI Server on %s:%s' % (self.host, self.port)
|
||||
|
||||
def start(self, modulename):
|
||||
mpconf = CONF_PATH
|
||||
if not os.path.isabs(mpconf):
|
||||
mpconf = os.path.join(curdir, mpconf)
|
||||
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
output = (self.template %
|
||||
{'port': self.port, 'testmod': modulename,
|
||||
'curdir': curdir})
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
|
||||
if result:
|
||||
print(result)
|
||||
|
||||
# Make a request so mod_wsgi starts up our app.
|
||||
# If we don't, concurrent initial requests will 404.
|
||||
portend.occupied('127.0.0.1', self.port, timeout=5)
|
||||
webtest.openURL('/ihopetheresnodefault', port=self.port)
|
||||
time.sleep(1)
|
||||
|
||||
def stop(self):
|
||||
"""Gracefully shutdown a server that is serving forever."""
|
||||
read_process(APACHE_PATH, '-k stop')
|
||||
|
||||
|
||||
loaded = False
|
||||
|
||||
|
||||
def application(environ, start_response):
|
||||
global loaded
|
||||
if not loaded:
|
||||
loaded = True
|
||||
modname = 'cherrypy.test.' + environ['testmod']
|
||||
mod = __import__(modname, globals(), locals(), [''])
|
||||
mod.setup_server()
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.error_file': os.path.join(curdir, 'test.error.log'),
|
||||
'log.access_file': os.path.join(curdir, 'test.access.log'),
|
||||
'environment': 'test_suite',
|
||||
'engine.SIGHUP': None,
|
||||
'engine.SIGTERM': None,
|
||||
})
|
||||
return cherrypy.tree(environ, start_response)
|
||||
161
lib/cherrypy/test/sessiondemo.py
Normal file
161
lib/cherrypy/test/sessiondemo.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/python
|
||||
"""A session demonstration app."""
|
||||
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import sessions
|
||||
|
||||
|
||||
page = """
|
||||
<html>
|
||||
<head>
|
||||
<style type='text/css'>
|
||||
table { border-collapse: collapse; border: 1px solid #663333; }
|
||||
th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
|
||||
td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
|
||||
border: 1px solid #663333; }
|
||||
.warn { font-family: serif; color: #990000; }
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function twodigit(d) { return d < 10 ? "0" + d : d; }
|
||||
function formattime(t) {
|
||||
var month = t.getUTCMonth() + 1;
|
||||
var day = t.getUTCDate();
|
||||
var year = t.getUTCFullYear();
|
||||
var hours = t.getUTCHours();
|
||||
var minutes = t.getUTCMinutes();
|
||||
return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
|
||||
hours + ":" + twodigit(minutes) + " UTC");
|
||||
}
|
||||
|
||||
function interval(s) {
|
||||
// Return the given interval (in seconds) as an English phrase
|
||||
var seconds = s %% 60;
|
||||
s = Math.floor(s / 60);
|
||||
var minutes = s %% 60;
|
||||
s = Math.floor(s / 60);
|
||||
var hours = s %% 24;
|
||||
var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
|
||||
var days = Math.floor(s / 24);
|
||||
if (days != 0) v = days + ' days, ' + v;
|
||||
return v;
|
||||
}
|
||||
|
||||
var fudge_seconds = 5;
|
||||
|
||||
function init() {
|
||||
// Set the content of the 'btime' cell.
|
||||
var currentTime = new Date();
|
||||
var bunixtime = Math.floor(currentTime.getTime() / 1000);
|
||||
|
||||
var v = formattime(currentTime);
|
||||
v += " (Unix time: " + bunixtime + ")";
|
||||
|
||||
var diff = Math.abs(%(serverunixtime)s - bunixtime);
|
||||
if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
|
||||
|
||||
document.getElementById('btime').innerHTML = v;
|
||||
|
||||
// Warn if response cookie expires is not close to one hour in the future.
|
||||
// Yes, we want this to happen when wit hit the 'Expire' link, too.
|
||||
var expires = Date.parse("%(expires)s") / 1000;
|
||||
var onehour = (60 * 60);
|
||||
if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
|
||||
diff = Math.floor(expires - bunixtime);
|
||||
if (expires > (bunixtime + onehour)) {
|
||||
var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
|
||||
} else {
|
||||
var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
|
||||
}
|
||||
document.getElementById('respcookiewarn').innerHTML = msg;
|
||||
}
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body onload='init()'>
|
||||
<h2>Session Demo</h2>
|
||||
<p>Reload this page. The session ID should not change from one reload to the next</p>
|
||||
<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
|
||||
<table>
|
||||
<tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
|
||||
<tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
|
||||
<tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
|
||||
<tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
|
||||
<tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
|
||||
<tr><th>Browser Time</th><td id='btime'> </td></tr>
|
||||
<tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
|
||||
<tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
|
||||
</table>
|
||||
</body></html>
|
||||
""" # noqa E501
|
||||
|
||||
|
||||
class Root(object):
|
||||
|
||||
def page(self):
|
||||
changemsg = []
|
||||
if cherrypy.session.id != cherrypy.session.originalid:
|
||||
if cherrypy.session.originalid is None:
|
||||
changemsg.append(
|
||||
'Created new session because no session id was given.')
|
||||
if cherrypy.session.missing:
|
||||
changemsg.append(
|
||||
'Created new session due to missing '
|
||||
'(expired or malicious) session.')
|
||||
if cherrypy.session.regenerated:
|
||||
changemsg.append('Application generated a new session.')
|
||||
|
||||
try:
|
||||
expires = cherrypy.response.cookie['session_id']['expires']
|
||||
except KeyError:
|
||||
expires = ''
|
||||
|
||||
return page % {
|
||||
'sessionid': cherrypy.session.id,
|
||||
'changemsg': '<br>'.join(changemsg),
|
||||
'respcookie': cherrypy.response.cookie.output(),
|
||||
'reqcookie': cherrypy.request.cookie.output(),
|
||||
'sessiondata': list(six.iteritems(cherrypy.session)),
|
||||
'servertime': (
|
||||
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
|
||||
),
|
||||
'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
|
||||
'cpversion': cherrypy.__version__,
|
||||
'pyversion': sys.version,
|
||||
'expires': expires,
|
||||
}
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
# Must modify data or the session will not be saved.
|
||||
cherrypy.session['color'] = 'green'
|
||||
return self.page()
|
||||
|
||||
@cherrypy.expose
|
||||
def expire(self):
|
||||
sessions.expire()
|
||||
return self.page()
|
||||
|
||||
@cherrypy.expose
|
||||
def regen(self):
|
||||
cherrypy.session.regenerate()
|
||||
# Must modify data or the session will not be saved.
|
||||
cherrypy.session['color'] = 'yellow'
|
||||
return self.page()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cherrypy.config.update({
|
||||
# 'environment': 'production',
|
||||
'log.screen': True,
|
||||
'tools.sessions.on': True,
|
||||
})
|
||||
cherrypy.quickstart(Root())
|
||||
5
lib/cherrypy/test/static/404.html
Normal file
5
lib/cherrypy/test/static/404.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>I couldn't find that thing you were looking for!</h1>
|
||||
</body>
|
||||
</html>
|
||||
BIN
lib/cherrypy/test/static/dirback.jpg
Normal file
BIN
lib/cherrypy/test/static/dirback.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
1
lib/cherrypy/test/static/index.html
Normal file
1
lib/cherrypy/test/static/index.html
Normal file
@@ -0,0 +1 @@
|
||||
Hello, world
|
||||
1
lib/cherrypy/test/style.css
Normal file
1
lib/cherrypy/test/style.css
Normal file
@@ -0,0 +1 @@
|
||||
Dummy stylesheet
|
||||
38
lib/cherrypy/test/test.pem
Normal file
38
lib/cherrypy/test/test.pem
Normal file
@@ -0,0 +1,38 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
|
||||
R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
|
||||
da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
|
||||
AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
|
||||
9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
|
||||
enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
|
||||
8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
|
||||
tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
|
||||
0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
|
||||
MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
|
||||
yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
|
||||
8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
|
||||
yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
|
||||
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
|
||||
MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
|
||||
MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
|
||||
cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
|
||||
bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
|
||||
FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
|
||||
cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
|
||||
ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
|
||||
C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
|
||||
KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
|
||||
2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
|
||||
/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
|
||||
MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
|
||||
CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
|
||||
BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
|
||||
8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
|
||||
D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
|
||||
NluCaWQys3MS
|
||||
-----END CERTIFICATE-----
|
||||
135
lib/cherrypy/test/test_auth_basic.py
Normal file
135
lib/cherrypy/test/test_auth_basic.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
from cherrypy.lib import auth_basic
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class BasicAuthTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'This is public.'
|
||||
|
||||
class BasicProtected:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
class BasicProtected2:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
class BasicProtected2_u:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
userpassdict = {'xuser': 'xpassword'}
|
||||
userhashdict = {'xuser': md5(b'xpassword').hexdigest()}
|
||||
userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()}
|
||||
|
||||
def checkpasshash(realm, user, password):
|
||||
p = userhashdict.get(user)
|
||||
return p and p == md5(ntob(password)).hexdigest() or False
|
||||
|
||||
def checkpasshash_u(realm, user, password):
|
||||
p = userhashdict_u.get(user)
|
||||
return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False
|
||||
|
||||
basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
|
||||
conf = {
|
||||
'/basic': {
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'wonderland',
|
||||
'tools.auth_basic.checkpassword': basic_checkpassword_dict
|
||||
},
|
||||
'/basic2': {
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'wonderland',
|
||||
'tools.auth_basic.checkpassword': checkpasshash,
|
||||
'tools.auth_basic.accept_charset': 'ISO-8859-1',
|
||||
},
|
||||
'/basic2_u': {
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'wonderland',
|
||||
'tools.auth_basic.checkpassword': checkpasshash_u,
|
||||
'tools.auth_basic.accept_charset': 'UTF-8',
|
||||
},
|
||||
}
|
||||
|
||||
root = Root()
|
||||
root.basic = BasicProtected()
|
||||
root.basic2 = BasicProtected2()
|
||||
root.basic2_u = BasicProtected2_u()
|
||||
cherrypy.tree.mount(root, config=conf)
|
||||
|
||||
def testPublic(self):
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
self.assertBody('This is public.')
|
||||
|
||||
def testBasic(self):
|
||||
self.getPage('/basic/')
|
||||
self.assertStatus(401)
|
||||
self.assertHeader(
|
||||
'WWW-Authenticate',
|
||||
'Basic realm="wonderland", charset="UTF-8"'
|
||||
)
|
||||
|
||||
self.getPage('/basic/',
|
||||
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
|
||||
self.assertStatus(401)
|
||||
|
||||
self.getPage('/basic/',
|
||||
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello xuser, you've been authorized.")
|
||||
|
||||
def testBasic2(self):
|
||||
self.getPage('/basic2/')
|
||||
self.assertStatus(401)
|
||||
self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
|
||||
|
||||
self.getPage('/basic2/',
|
||||
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
|
||||
self.assertStatus(401)
|
||||
|
||||
self.getPage('/basic2/',
|
||||
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello xuser, you've been authorized.")
|
||||
|
||||
def testBasic2_u(self):
|
||||
self.getPage('/basic2_u/')
|
||||
self.assertStatus(401)
|
||||
self.assertHeader(
|
||||
'WWW-Authenticate',
|
||||
'Basic realm="wonderland", charset="UTF-8"'
|
||||
)
|
||||
|
||||
self.getPage('/basic2_u/',
|
||||
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')])
|
||||
self.assertStatus(401)
|
||||
|
||||
self.getPage('/basic2_u/',
|
||||
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello xюзер, you've been authorized.")
|
||||
134
lib/cherrypy/test/test_auth_digest.py
Normal file
134
lib/cherrypy/test/test_auth_digest.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
import six
|
||||
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import auth_digest
|
||||
from cherrypy._cpcompat import ntob
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
def _fetch_users():
|
||||
return {'test': 'test', '☃йюзер': 'їпароль'}
|
||||
|
||||
|
||||
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users())
|
||||
|
||||
|
||||
class DigestAuthTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'This is public.'
|
||||
|
||||
class DigestProtected:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, *args, **kwargs):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
conf = {'/digest': {'tools.auth_digest.on': True,
|
||||
'tools.auth_digest.realm': 'localhost',
|
||||
'tools.auth_digest.get_ha1': get_ha1,
|
||||
'tools.auth_digest.key': 'a565c27146791cfb',
|
||||
'tools.auth_digest.debug': True,
|
||||
'tools.auth_digest.accept_charset': 'UTF-8'}}
|
||||
|
||||
root = Root()
|
||||
root.digest = DigestProtected()
|
||||
cherrypy.tree.mount(root, config=conf)
|
||||
|
||||
def testPublic(self):
|
||||
self.getPage('/')
|
||||
assert self.status == '200 OK'
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
assert self.body == b'This is public.'
|
||||
|
||||
def _test_parametric_digest(self, username, realm):
|
||||
test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path'
|
||||
|
||||
self.getPage(test_uri)
|
||||
assert self.status_code == 401
|
||||
|
||||
msg = 'Digest authentification scheme was not found'
|
||||
www_auth_digest = tuple(filter(
|
||||
lambda kv: kv[0].lower() == 'www-authenticate'
|
||||
and kv[1].startswith('Digest '),
|
||||
self.headers,
|
||||
))
|
||||
assert len(www_auth_digest) == 1, msg
|
||||
|
||||
items = www_auth_digest[0][-1][7:].split(', ')
|
||||
tokens = {}
|
||||
for item in items:
|
||||
key, value = item.split('=')
|
||||
tokens[key.lower()] = value
|
||||
|
||||
assert tokens['realm'] == '"localhost"'
|
||||
assert tokens['algorithm'] == '"MD5"'
|
||||
assert tokens['qop'] == '"auth"'
|
||||
assert tokens['charset'] == '"UTF-8"'
|
||||
|
||||
nonce = tokens['nonce'].strip('"')
|
||||
|
||||
# Test user agent response with a wrong value for 'realm'
|
||||
base_auth = ('Digest username="%s", '
|
||||
'realm="%s", '
|
||||
'nonce="%s", '
|
||||
'uri="%s", '
|
||||
'algorithm=MD5, '
|
||||
'response="%s", '
|
||||
'qop=auth, '
|
||||
'nc=%s, '
|
||||
'cnonce="1522e61005789929"')
|
||||
|
||||
encoded_user = username
|
||||
if six.PY3:
|
||||
encoded_user = encoded_user.encode('utf-8')
|
||||
encoded_user = encoded_user.decode('latin1')
|
||||
auth_header = base_auth % (
|
||||
encoded_user, realm, nonce, test_uri,
|
||||
'11111111111111111111111111111111', '00000001',
|
||||
)
|
||||
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
|
||||
# calculate the response digest
|
||||
ha1 = get_ha1(auth.realm, auth.username)
|
||||
response = auth.request_digest(ha1)
|
||||
auth_header = base_auth % (
|
||||
encoded_user, realm, nonce, test_uri,
|
||||
response, '00000001',
|
||||
)
|
||||
self.getPage(test_uri, [('Authorization', auth_header)])
|
||||
|
||||
def test_wrong_realm(self):
|
||||
# send response with correct response digest, but wrong realm
|
||||
self._test_parametric_digest(username='test', realm='wrong realm')
|
||||
assert self.status_code == 401
|
||||
|
||||
def test_ascii_user(self):
|
||||
self._test_parametric_digest(username='test', realm='localhost')
|
||||
assert self.status == '200 OK'
|
||||
assert self.body == b"Hello test, you've been authorized."
|
||||
|
||||
def test_unicode_user(self):
|
||||
self._test_parametric_digest(username='☃йюзер', realm='localhost')
|
||||
assert self.status == '200 OK'
|
||||
assert self.body == ntob(
|
||||
"Hello ☃йюзер, you've been authorized.", 'utf-8',
|
||||
)
|
||||
|
||||
def test_wrong_scheme(self):
|
||||
basic_auth = {
|
||||
'Authorization': 'Basic foo:bar',
|
||||
}
|
||||
self.getPage('/digest/', headers=list(basic_auth.items()))
|
||||
assert self.status_code == 401
|
||||
274
lib/cherrypy/test/test_bus.py
Normal file
274
lib/cherrypy/test/test_bus.py
Normal file
@@ -0,0 +1,274 @@
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from cherrypy.process import wspbus
|
||||
|
||||
|
||||
msg = 'Listener %d on channel %s: %s.'
|
||||
|
||||
|
||||
class PublishSubscribeTests(unittest.TestCase):
|
||||
|
||||
def get_listener(self, channel, index):
|
||||
def listener(arg=None):
|
||||
self.responses.append(msg % (index, channel, arg))
|
||||
return listener
|
||||
|
||||
def test_builtin_channels(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
|
||||
for channel in b.listeners:
|
||||
for index, priority in enumerate([100, 50, 0, 51]):
|
||||
b.subscribe(channel,
|
||||
self.get_listener(channel, index), priority)
|
||||
|
||||
for channel in b.listeners:
|
||||
b.publish(channel)
|
||||
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
|
||||
b.publish(channel, arg=79347)
|
||||
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
|
||||
def test_custom_channels(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
|
||||
custom_listeners = ('hugh', 'louis', 'dewey')
|
||||
for channel in custom_listeners:
|
||||
for index, priority in enumerate([None, 10, 60, 40]):
|
||||
b.subscribe(channel,
|
||||
self.get_listener(channel, index), priority)
|
||||
|
||||
for channel in custom_listeners:
|
||||
b.publish(channel, 'ah so')
|
||||
expected.extend([msg % (i, channel, 'ah so')
|
||||
for i in (1, 3, 0, 2)])
|
||||
b.publish(channel)
|
||||
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
|
||||
def test_listener_errors(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
channels = [c for c in b.listeners if c != 'log']
|
||||
|
||||
for channel in channels:
|
||||
b.subscribe(channel, self.get_listener(channel, 1))
|
||||
# This will break since the lambda takes no args.
|
||||
b.subscribe(channel, lambda: None, priority=20)
|
||||
|
||||
for channel in channels:
|
||||
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
|
||||
expected.append(msg % (1, channel, 123))
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
|
||||
|
||||
class BusMethodTests(unittest.TestCase):
|
||||
|
||||
def log(self, bus):
|
||||
self._log_entries = []
|
||||
|
||||
def logit(msg, level):
|
||||
self._log_entries.append(msg)
|
||||
bus.subscribe('log', logit)
|
||||
|
||||
def assertLog(self, entries):
|
||||
self.assertEqual(self._log_entries, entries)
|
||||
|
||||
def get_listener(self, channel, index):
|
||||
def listener(arg=None):
|
||||
self.responses.append(msg % (index, channel, arg))
|
||||
return listener
|
||||
|
||||
def test_start(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('start', self.get_listener('start', index))
|
||||
|
||||
b.start()
|
||||
try:
|
||||
# The start method MUST call all 'start' listeners.
|
||||
self.assertEqual(
|
||||
set(self.responses),
|
||||
set([msg % (i, 'start', None) for i in range(num)]))
|
||||
# The start method MUST move the state to STARTED
|
||||
# (or EXITING, if errors occur)
|
||||
self.assertEqual(b.state, b.states.STARTED)
|
||||
# The start method MUST log its states.
|
||||
self.assertLog(['Bus STARTING', 'Bus STARTED'])
|
||||
finally:
|
||||
# Exit so the atexit handler doesn't complain.
|
||||
b.exit()
|
||||
|
||||
def test_stop(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('stop', self.get_listener('stop', index))
|
||||
|
||||
b.stop()
|
||||
|
||||
# The stop method MUST call all 'stop' listeners.
|
||||
self.assertEqual(set(self.responses),
|
||||
set([msg % (i, 'stop', None) for i in range(num)]))
|
||||
# The stop method MUST move the state to STOPPED
|
||||
self.assertEqual(b.state, b.states.STOPPED)
|
||||
# The stop method MUST log its states.
|
||||
self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
|
||||
|
||||
def test_graceful(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('graceful', self.get_listener('graceful', index))
|
||||
|
||||
b.graceful()
|
||||
|
||||
# The graceful method MUST call all 'graceful' listeners.
|
||||
self.assertEqual(
|
||||
set(self.responses),
|
||||
set([msg % (i, 'graceful', None) for i in range(num)]))
|
||||
# The graceful method MUST log its states.
|
||||
self.assertLog(['Bus graceful'])
|
||||
|
||||
def test_exit(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('stop', self.get_listener('stop', index))
|
||||
b.subscribe('exit', self.get_listener('exit', index))
|
||||
|
||||
b.exit()
|
||||
|
||||
# The exit method MUST call all 'stop' listeners,
|
||||
# and then all 'exit' listeners.
|
||||
self.assertEqual(set(self.responses),
|
||||
set([msg % (i, 'stop', None) for i in range(num)] +
|
||||
[msg % (i, 'exit', None) for i in range(num)]))
|
||||
# The exit method MUST move the state to EXITING
|
||||
self.assertEqual(b.state, b.states.EXITING)
|
||||
# The exit method MUST log its states.
|
||||
self.assertLog(
|
||||
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
|
||||
|
||||
def test_wait(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
def f(method):
|
||||
time.sleep(0.2)
|
||||
getattr(b, method)()
|
||||
|
||||
for method, states in [('start', [b.states.STARTED]),
|
||||
('stop', [b.states.STOPPED]),
|
||||
('start',
|
||||
[b.states.STARTING, b.states.STARTED]),
|
||||
('exit', [b.states.EXITING]),
|
||||
]:
|
||||
threading.Thread(target=f, args=(method,)).start()
|
||||
b.wait(states)
|
||||
|
||||
# The wait method MUST wait for the given state(s).
|
||||
if b.state not in states:
|
||||
self.fail('State %r not in %r' % (b.state, states))
|
||||
|
||||
def test_block(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
def f():
|
||||
time.sleep(0.2)
|
||||
b.exit()
|
||||
|
||||
def g():
|
||||
time.sleep(0.4)
|
||||
threading.Thread(target=f).start()
|
||||
threading.Thread(target=g).start()
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
self.assertEqual(len(threads), 3)
|
||||
|
||||
b.block()
|
||||
|
||||
# The block method MUST wait for the EXITING state.
|
||||
self.assertEqual(b.state, b.states.EXITING)
|
||||
# The block method MUST wait for ALL non-main, non-daemon threads to
|
||||
# finish.
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
self.assertEqual(len(threads), 1)
|
||||
# The last message will mention an indeterminable thread name; ignore
|
||||
# it
|
||||
self.assertEqual(self._log_entries[:-1],
|
||||
['Bus STOPPING', 'Bus STOPPED',
|
||||
'Bus EXITING', 'Bus EXITED',
|
||||
'Waiting for child threads to terminate...'])
|
||||
|
||||
def test_start_with_callback(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
try:
|
||||
events = []
|
||||
|
||||
def f(*args, **kwargs):
|
||||
events.append(('f', args, kwargs))
|
||||
|
||||
def g():
|
||||
events.append('g')
|
||||
b.subscribe('start', g)
|
||||
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
|
||||
# Give wait() time to run f()
|
||||
time.sleep(0.2)
|
||||
|
||||
# The callback method MUST wait for the STARTED state.
|
||||
self.assertEqual(b.state, b.states.STARTED)
|
||||
# The callback method MUST run after all start methods.
|
||||
self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
|
||||
finally:
|
||||
b.exit()
|
||||
|
||||
def test_log(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
self.assertLog([])
|
||||
|
||||
# Try a normal message.
|
||||
expected = []
|
||||
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
|
||||
b.log(msg)
|
||||
expected.append(msg)
|
||||
self.assertLog(expected)
|
||||
|
||||
# Try an error message
|
||||
try:
|
||||
foo
|
||||
except NameError:
|
||||
b.log('You are lost and gone forever', traceback=True)
|
||||
lastmsg = self._log_entries[-1]
|
||||
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
|
||||
self.fail('Last log message %r did not contain '
|
||||
'the expected traceback.' % lastmsg)
|
||||
else:
|
||||
self.fail('NameError was not raised as expected.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
392
lib/cherrypy/test/test_caching.py
Normal file
392
lib/cherrypy/test/test_caching.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import datetime
|
||||
from itertools import count
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from six.moves import range
|
||||
from six.moves import urllib
|
||||
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
gif_bytes = (
|
||||
b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
|
||||
)
|
||||
|
||||
|
||||
class CacheTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
@cherrypy.config(**{'tools.caching.on': True})
|
||||
class Root:
|
||||
|
||||
def __init__(self):
|
||||
self.counter = 0
|
||||
self.control_counter = 0
|
||||
self.longlock = threading.Lock()
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
self.counter += 1
|
||||
msg = 'visit #%s' % self.counter
|
||||
return msg
|
||||
|
||||
@cherrypy.expose
|
||||
def control(self):
|
||||
self.control_counter += 1
|
||||
return 'visit #%s' % self.control_counter
|
||||
|
||||
@cherrypy.expose
|
||||
def a_gif(self):
|
||||
cherrypy.response.headers[
|
||||
'Last-Modified'] = httputil.HTTPDate()
|
||||
return gif_bytes
|
||||
|
||||
@cherrypy.expose
|
||||
def long_process(self, seconds='1'):
|
||||
try:
|
||||
self.longlock.acquire()
|
||||
time.sleep(float(seconds))
|
||||
finally:
|
||||
self.longlock.release()
|
||||
return 'success!'
|
||||
|
||||
@cherrypy.expose
|
||||
def clear_cache(self, path):
|
||||
cherrypy._cache.store[cherrypy.request.base + path].clear()
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.caching.on': True,
|
||||
'tools.response_headers.on': True,
|
||||
'tools.response_headers.headers': [
|
||||
('Vary', 'Our-Varying-Header')
|
||||
],
|
||||
})
|
||||
class VaryHeaderCachingServer(object):
|
||||
|
||||
def __init__(self):
|
||||
self.counter = count(1)
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'visit #%s' % next(self.counter)
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60,
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.root': curdir,
|
||||
})
|
||||
class UnCached(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.expires.secs': 0})
|
||||
def force(self):
|
||||
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
|
||||
self._cp_config['tools.expires.force'] = True
|
||||
self._cp_config['tools.expires.secs'] = 0
|
||||
return 'being forceful'
|
||||
|
||||
@cherrypy.expose
|
||||
def dynamic(self):
|
||||
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
|
||||
cherrypy.response.headers['Cache-Control'] = 'private'
|
||||
return 'D-d-d-dynamic!'
|
||||
|
||||
@cherrypy.expose
|
||||
def cacheable(self):
|
||||
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
|
||||
return "Hi, I'm cacheable."
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.expires.secs': 86400})
|
||||
def specific(self):
|
||||
cherrypy.response.headers[
|
||||
'Etag'] = 'need_this_to_make_me_cacheable'
|
||||
return 'I am being specific'
|
||||
|
||||
class Foo(object):
|
||||
pass
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.expires.secs': Foo()})
|
||||
def wrongtype(self):
|
||||
cherrypy.response.headers[
|
||||
'Etag'] = 'need_this_to_make_me_cacheable'
|
||||
return 'Woops'
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.gzip.mime_types': ['text/*', 'image/*'],
|
||||
'tools.caching.on': True,
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.root': curdir
|
||||
})
|
||||
class GzipStaticCache(object):
|
||||
pass
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.tree.mount(UnCached(), '/expires')
|
||||
cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
|
||||
cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache')
|
||||
cherrypy.config.update({'tools.gzip.on': True})
|
||||
|
||||
def testCaching(self):
|
||||
elapsed = 0.0
|
||||
for trial in range(10):
|
||||
self.getPage('/')
|
||||
# The response should be the same every time,
|
||||
# except for the Age response header.
|
||||
self.assertBody('visit #1')
|
||||
if trial != 0:
|
||||
age = int(self.assertHeader('Age'))
|
||||
self.assert_(age >= elapsed)
|
||||
elapsed = age
|
||||
|
||||
# POST, PUT, DELETE should not be cached.
|
||||
self.getPage('/', method='POST')
|
||||
self.assertBody('visit #2')
|
||||
# Because gzip is turned on, the Vary header should always Vary for
|
||||
# content-encoding
|
||||
self.assertHeader('Vary', 'Accept-Encoding')
|
||||
# The previous request should have invalidated the cache,
|
||||
# so this request will recalc the response.
|
||||
self.getPage('/', method='GET')
|
||||
self.assertBody('visit #3')
|
||||
# ...but this request should get the cached copy.
|
||||
self.getPage('/', method='GET')
|
||||
self.assertBody('visit #3')
|
||||
self.getPage('/', method='DELETE')
|
||||
self.assertBody('visit #4')
|
||||
|
||||
# The previous request should have invalidated the cache,
|
||||
# so this request will recalc the response.
|
||||
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertHeader('Content-Encoding', 'gzip')
|
||||
self.assertHeader('Vary')
|
||||
self.assertEqual(
|
||||
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
|
||||
|
||||
# Now check that a second request gets the gzip header and gzipped body
|
||||
# This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
|
||||
# response body was being gzipped a second time.
|
||||
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertHeader('Content-Encoding', 'gzip')
|
||||
self.assertEqual(
|
||||
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
|
||||
|
||||
# Now check that a third request that doesn't accept gzip
|
||||
# skips the cache (because the 'Vary' header denies it).
|
||||
self.getPage('/', method='GET')
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
self.assertBody('visit #6')
|
||||
|
||||
def testVaryHeader(self):
|
||||
self.getPage('/varying_headers/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
|
||||
self.assertBody('visit #1')
|
||||
|
||||
# Now check that different 'Vary'-fields don't evict each other.
|
||||
# This test creates 2 requests with different 'Our-Varying-Header'
|
||||
# and then tests if the first one still exists.
|
||||
self.getPage('/varying_headers/',
|
||||
headers=[('Our-Varying-Header', 'request 2')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('visit #2')
|
||||
|
||||
self.getPage('/varying_headers/',
|
||||
headers=[('Our-Varying-Header', 'request 2')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('visit #2')
|
||||
|
||||
self.getPage('/varying_headers/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('visit #1')
|
||||
|
||||
def testExpiresTool(self):
|
||||
# test setting an expires header
|
||||
self.getPage('/expires/specific')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Expires')
|
||||
|
||||
# test exceptions for bad time values
|
||||
self.getPage('/expires/wrongtype')
|
||||
self.assertStatus(500)
|
||||
self.assertInBody('TypeError')
|
||||
|
||||
# static content should not have "cache prevention" headers
|
||||
self.getPage('/expires/index.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertNoHeader('Pragma')
|
||||
self.assertNoHeader('Cache-Control')
|
||||
self.assertHeader('Expires')
|
||||
|
||||
# dynamic content that sets indicators should not have
|
||||
# "cache prevention" headers
|
||||
self.getPage('/expires/cacheable')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertNoHeader('Pragma')
|
||||
self.assertNoHeader('Cache-Control')
|
||||
self.assertHeader('Expires')
|
||||
|
||||
self.getPage('/expires/dynamic')
|
||||
self.assertBody('D-d-d-dynamic!')
|
||||
# the Cache-Control header should be untouched
|
||||
self.assertHeader('Cache-Control', 'private')
|
||||
self.assertHeader('Expires')
|
||||
|
||||
# configure the tool to ignore indicators and replace existing headers
|
||||
self.getPage('/expires/force')
|
||||
self.assertStatus('200 OK')
|
||||
# This also gives us a chance to test 0 expiry with no other headers
|
||||
self.assertHeader('Pragma', 'no-cache')
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
|
||||
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
|
||||
|
||||
# static content should now have "cache prevention" headers
|
||||
self.getPage('/expires/index.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Pragma', 'no-cache')
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
|
||||
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
|
||||
|
||||
# the cacheable handler should now have "cache prevention" headers
|
||||
self.getPage('/expires/cacheable')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Pragma', 'no-cache')
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
|
||||
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
|
||||
|
||||
self.getPage('/expires/dynamic')
|
||||
self.assertBody('D-d-d-dynamic!')
|
||||
# dynamic sets Cache-Control to private but it should be
|
||||
# overwritten here ...
|
||||
self.assertHeader('Pragma', 'no-cache')
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
|
||||
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
|
||||
|
||||
def _assert_resp_len_and_enc_for_gzip(self, uri):
|
||||
"""
|
||||
Test that after querying gzipped content it's remains valid in
|
||||
cache and available non-gzipped as well.
|
||||
"""
|
||||
ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')]
|
||||
content_len = None
|
||||
|
||||
for _ in range(3):
|
||||
self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS)
|
||||
|
||||
if content_len is not None:
|
||||
# all requests should get the same length
|
||||
self.assertHeader('Content-Length', content_len)
|
||||
self.assertHeader('Content-Encoding', 'gzip')
|
||||
|
||||
content_len = dict(self.headers)['Content-Length']
|
||||
|
||||
# check that we can still get non-gzipped version
|
||||
self.getPage(uri, method='GET')
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
# non-gzipped version should have a different content length
|
||||
self.assertNoHeaderItemValue('Content-Length', content_len)
|
||||
|
||||
def testGzipStaticCache(self):
|
||||
"""Test that cache and gzip tools play well together when both enabled.
|
||||
|
||||
Ref GitHub issue #1190.
|
||||
"""
|
||||
GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}'
|
||||
resource_files = ('index.html', 'dirback.jpg')
|
||||
|
||||
for f in resource_files:
|
||||
uri = GZIP_STATIC_CACHE_TMPL.format(f)
|
||||
self._assert_resp_len_and_enc_for_gzip(uri)
|
||||
|
||||
def testLastModified(self):
|
||||
self.getPage('/a.gif')
|
||||
self.assertStatus(200)
|
||||
self.assertBody(gif_bytes)
|
||||
lm1 = self.assertHeader('Last-Modified')
|
||||
|
||||
# this request should get the cached copy.
|
||||
self.getPage('/a.gif')
|
||||
self.assertStatus(200)
|
||||
self.assertBody(gif_bytes)
|
||||
self.assertHeader('Age')
|
||||
lm2 = self.assertHeader('Last-Modified')
|
||||
self.assertEqual(lm1, lm2)
|
||||
|
||||
# this request should match the cached copy, but raise 304.
|
||||
self.getPage('/a.gif', [('If-Modified-Since', lm1)])
|
||||
self.assertStatus(304)
|
||||
self.assertNoHeader('Last-Modified')
|
||||
if not getattr(cherrypy.server, 'using_apache', False):
|
||||
self.assertHeader('Age')
|
||||
|
||||
@pytest.mark.xfail(reason='#1536')
|
||||
def test_antistampede(self):
|
||||
SECONDS = 4
|
||||
slow_url = '/long_process?seconds={SECONDS}'.format(**locals())
|
||||
# We MUST make an initial synchronous request in order to create the
|
||||
# AntiStampedeCache object, and populate its selecting_headers,
|
||||
# before the actual stampede.
|
||||
self.getPage(slow_url)
|
||||
self.assertBody('success!')
|
||||
path = urllib.parse.quote(slow_url, safe='')
|
||||
self.getPage('/clear_cache?path=' + path)
|
||||
self.assertStatus(200)
|
||||
|
||||
start = datetime.datetime.now()
|
||||
|
||||
def run():
|
||||
self.getPage(slow_url)
|
||||
# The response should be the same every time
|
||||
self.assertBody('success!')
|
||||
ts = [threading.Thread(target=run) for i in range(100)]
|
||||
for t in ts:
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join()
|
||||
finish = datetime.datetime.now()
|
||||
# Allow for overhead, two seconds for slow hosts
|
||||
allowance = SECONDS + 2
|
||||
self.assertEqualDates(start, finish, seconds=allowance)
|
||||
|
||||
def test_cache_control(self):
|
||||
self.getPage('/control')
|
||||
self.assertBody('visit #1')
|
||||
self.getPage('/control')
|
||||
self.assertBody('visit #1')
|
||||
|
||||
self.getPage('/control', headers=[('Cache-Control', 'no-cache')])
|
||||
self.assertBody('visit #2')
|
||||
self.getPage('/control')
|
||||
self.assertBody('visit #2')
|
||||
|
||||
self.getPage('/control', headers=[('Pragma', 'no-cache')])
|
||||
self.assertBody('visit #3')
|
||||
self.getPage('/control')
|
||||
self.assertBody('visit #3')
|
||||
|
||||
time.sleep(1)
|
||||
self.getPage('/control', headers=[('Cache-Control', 'max-age=0')])
|
||||
self.assertBody('visit #4')
|
||||
self.getPage('/control')
|
||||
self.assertBody('visit #4')
|
||||
34
lib/cherrypy/test/test_compat.py
Normal file
34
lib/cherrypy/test/test_compat.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Test Python 2/3 compatibility module."""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
import six
|
||||
|
||||
from cherrypy import _cpcompat as compat
|
||||
|
||||
|
||||
class StringTester(unittest.TestCase):
|
||||
"""Tests for string conversion."""
|
||||
|
||||
@pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
|
||||
def test_ntob_non_native(self):
|
||||
"""ntob should raise an Exception on unicode.
|
||||
|
||||
(Python 2 only)
|
||||
|
||||
See #1132 for discussion.
|
||||
"""
|
||||
self.assertRaises(TypeError, compat.ntob, 'fight')
|
||||
|
||||
|
||||
class EscapeTester(unittest.TestCase):
|
||||
"""Class to test escape_html function from _cpcompat."""
|
||||
|
||||
def test_escape_quote(self):
|
||||
"""test_escape_quote - Verify the output for &<>"' chars."""
|
||||
self.assertEqual(
|
||||
"""xx&<>"aa'""",
|
||||
compat.escape_html("""xx&<>"aa'"""),
|
||||
)
|
||||
303
lib/cherrypy/test/test_config.py
Normal file
303
lib/cherrypy/test/test_config.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""Tests for the CherryPy configuration system."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
def StringIOFromNative(x):
|
||||
return io.StringIO(six.text_type(x))
|
||||
|
||||
|
||||
def setup_server():
|
||||
|
||||
@cherrypy.config(foo='this', bar='that')
|
||||
class Root:
|
||||
|
||||
def __init__(self):
|
||||
cherrypy.config.namespaces['db'] = self.db_namespace
|
||||
|
||||
def db_namespace(self, k, v):
|
||||
if k == 'scheme':
|
||||
self.db = v
|
||||
|
||||
@cherrypy.expose(alias=('global_', 'xyz'))
|
||||
def index(self, key):
|
||||
return cherrypy.request.config.get(key, 'None')
|
||||
|
||||
@cherrypy.expose
|
||||
def repr(self, key):
|
||||
return repr(cherrypy.request.config.get(key, None))
|
||||
|
||||
@cherrypy.expose
|
||||
def dbscheme(self):
|
||||
return self.db
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']})
|
||||
def plain(self, x):
|
||||
return x
|
||||
|
||||
favicon_ico = cherrypy.tools.staticfile.handler(
|
||||
filename=os.path.join(localDir, '../favicon.ico'))
|
||||
|
||||
@cherrypy.config(foo='this2', baz='that2')
|
||||
class Foo:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, key):
|
||||
return cherrypy.request.config.get(key, 'None')
|
||||
nex = index
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'response.headers.X-silly': 'sillyval'})
|
||||
def silly(self):
|
||||
return 'Hello world'
|
||||
|
||||
# Test the expose and config decorators
|
||||
@cherrypy.config(foo='this3', **{'bax': 'this4'})
|
||||
@cherrypy.expose
|
||||
def bar(self, key):
|
||||
return repr(cherrypy.request.config.get(key, None))
|
||||
|
||||
class Another:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, key):
|
||||
return str(cherrypy.request.config.get(key, 'None'))
|
||||
|
||||
def raw_namespace(key, value):
|
||||
if key == 'input.map':
|
||||
handler = cherrypy.request.handler
|
||||
|
||||
def wrapper():
|
||||
params = cherrypy.request.params
|
||||
for name, coercer in list(value.items()):
|
||||
try:
|
||||
params[name] = coercer(params[name])
|
||||
except KeyError:
|
||||
pass
|
||||
return handler()
|
||||
cherrypy.request.handler = wrapper
|
||||
elif key == 'output':
|
||||
handler = cherrypy.request.handler
|
||||
|
||||
def wrapper():
|
||||
# 'value' is a type (like int or str).
|
||||
return value(handler())
|
||||
cherrypy.request.handler = wrapper
|
||||
|
||||
@cherrypy.config(**{'raw.output': repr})
|
||||
class Raw:
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'raw.input.map': {'num': int}})
|
||||
def incr(self, num):
|
||||
return num + 1
|
||||
|
||||
if not six.PY3:
|
||||
thing3 = "thing3: unicode('test', errors='ignore')"
|
||||
else:
|
||||
thing3 = ''
|
||||
|
||||
ioconf = StringIOFromNative("""
|
||||
[/]
|
||||
neg: -1234
|
||||
filename: os.path.join(sys.prefix, "hello.py")
|
||||
thing1: cherrypy.lib.httputil.response_codes[404]
|
||||
thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
|
||||
%s
|
||||
complex: 3+2j
|
||||
mul: 6*3
|
||||
ones: "11"
|
||||
twos: "22"
|
||||
stradd: %%(ones)s + %%(twos)s + "33"
|
||||
|
||||
[/favicon.ico]
|
||||
tools.staticfile.filename = %r
|
||||
""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
|
||||
|
||||
root = Root()
|
||||
root.foo = Foo()
|
||||
root.raw = Raw()
|
||||
app = cherrypy.tree.mount(root, config=ioconf)
|
||||
app.request_class.namespaces['raw'] = raw_namespace
|
||||
|
||||
cherrypy.tree.mount(Another(), '/another')
|
||||
cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
|
||||
'db.scheme': r'sqlite///memory',
|
||||
})
|
||||
|
||||
|
||||
# Client-side code #
|
||||
|
||||
|
||||
class ConfigTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testConfig(self):
|
||||
tests = [
|
||||
('/', 'nex', 'None'),
|
||||
('/', 'foo', 'this'),
|
||||
('/', 'bar', 'that'),
|
||||
('/xyz', 'foo', 'this'),
|
||||
('/foo/', 'foo', 'this2'),
|
||||
('/foo/', 'bar', 'that'),
|
||||
('/foo/', 'bax', 'None'),
|
||||
('/foo/bar', 'baz', "'that2'"),
|
||||
('/foo/nex', 'baz', 'that2'),
|
||||
# If 'foo' == 'this', then the mount point '/another' leaks into
|
||||
# '/'.
|
||||
('/another/', 'foo', 'None'),
|
||||
]
|
||||
for path, key, expected in tests:
|
||||
self.getPage(path + '?key=' + key)
|
||||
self.assertBody(expected)
|
||||
|
||||
expectedconf = {
|
||||
# From CP defaults
|
||||
'tools.log_headers.on': False,
|
||||
'tools.log_tracebacks.on': True,
|
||||
'request.show_tracebacks': True,
|
||||
'log.screen': False,
|
||||
'environment': 'test_suite',
|
||||
'engine.autoreload.on': False,
|
||||
# From global config
|
||||
'luxuryyacht': 'throatwobblermangrove',
|
||||
# From Root._cp_config
|
||||
'bar': 'that',
|
||||
# From Foo._cp_config
|
||||
'baz': 'that2',
|
||||
# From Foo.bar._cp_config
|
||||
'foo': 'this3',
|
||||
'bax': 'this4',
|
||||
}
|
||||
for key, expected in expectedconf.items():
|
||||
self.getPage('/foo/bar?key=' + key)
|
||||
self.assertBody(repr(expected))
|
||||
|
||||
def testUnrepr(self):
|
||||
self.getPage('/repr?key=neg')
|
||||
self.assertBody('-1234')
|
||||
|
||||
self.getPage('/repr?key=filename')
|
||||
self.assertBody(repr(os.path.join(sys.prefix, 'hello.py')))
|
||||
|
||||
self.getPage('/repr?key=thing1')
|
||||
self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
|
||||
|
||||
if not getattr(cherrypy.server, 'using_apache', False):
|
||||
# The object ID's won't match up when using Apache, since the
|
||||
# server and client are running in different processes.
|
||||
self.getPage('/repr?key=thing2')
|
||||
from cherrypy.tutorial import thing2
|
||||
self.assertBody(repr(thing2))
|
||||
|
||||
if not six.PY3:
|
||||
self.getPage('/repr?key=thing3')
|
||||
self.assertBody(repr(six.text_type('test')))
|
||||
|
||||
self.getPage('/repr?key=complex')
|
||||
self.assertBody('(3+2j)')
|
||||
|
||||
self.getPage('/repr?key=mul')
|
||||
self.assertBody('18')
|
||||
|
||||
self.getPage('/repr?key=stradd')
|
||||
self.assertBody(repr('112233'))
|
||||
|
||||
def testRespNamespaces(self):
|
||||
self.getPage('/foo/silly')
|
||||
self.assertHeader('X-silly', 'sillyval')
|
||||
self.assertBody('Hello world')
|
||||
|
||||
def testCustomNamespaces(self):
|
||||
self.getPage('/raw/incr?num=12')
|
||||
self.assertBody('13')
|
||||
|
||||
self.getPage('/dbscheme')
|
||||
self.assertBody(r'sqlite///memory')
|
||||
|
||||
def testHandlerToolConfigOverride(self):
|
||||
# Assert that config overrides tool constructor args. Above, we set
|
||||
# the favicon in the page handler to be '../favicon.ico',
|
||||
# but then overrode it in config to be './static/dirback.jpg'.
|
||||
self.getPage('/favicon.ico')
|
||||
self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
|
||||
'rb').read())
|
||||
|
||||
def test_request_body_namespace(self):
|
||||
self.getPage('/plain', method='POST', headers=[
|
||||
('Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', '13')],
|
||||
body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')
|
||||
self.assertBody('abc')
|
||||
|
||||
|
||||
class VariableSubstitutionTests(unittest.TestCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_config(self):
|
||||
from textwrap import dedent
|
||||
|
||||
# variable substitution with [DEFAULT]
|
||||
conf = dedent("""
|
||||
[DEFAULT]
|
||||
dir = "/some/dir"
|
||||
my.dir = %(dir)s + "/sub"
|
||||
|
||||
[my]
|
||||
my.dir = %(dir)s + "/my/dir"
|
||||
my.dir2 = %(my.dir)s + '/dir2'
|
||||
|
||||
""")
|
||||
|
||||
fp = StringIOFromNative(conf)
|
||||
|
||||
cherrypy.config.update(fp)
|
||||
self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir')
|
||||
self.assertEqual(cherrypy.config['my']
|
||||
['my.dir2'], '/some/dir/my/dir/dir2')
|
||||
|
||||
|
||||
class CallablesInConfigTest(unittest.TestCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_call_with_literal_dict(self):
|
||||
from textwrap import dedent
|
||||
conf = dedent("""
|
||||
[my]
|
||||
value = dict(**{'foo': 'bar'})
|
||||
""")
|
||||
fp = StringIOFromNative(conf)
|
||||
cherrypy.config.update(fp)
|
||||
self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
|
||||
|
||||
def test_call_with_kwargs(self):
|
||||
from textwrap import dedent
|
||||
conf = dedent("""
|
||||
[my]
|
||||
value = dict(foo="buzz", **cherrypy._test_dict)
|
||||
""")
|
||||
test_dict = {
|
||||
'foo': 'bar',
|
||||
'bar': 'foo',
|
||||
'fizz': 'buzz'
|
||||
}
|
||||
cherrypy._test_dict = test_dict
|
||||
fp = StringIOFromNative(conf)
|
||||
cherrypy.config.update(fp)
|
||||
test_dict['foo'] = 'buzz'
|
||||
self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
|
||||
self.assertEqual(cherrypy.config['my']['value'], test_dict)
|
||||
del cherrypy._test_dict
|
||||
126
lib/cherrypy/test/test_config_server.py
Normal file
126
lib/cherrypy/test/test_config_server.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""Tests for the CherryPy configuration system."""
|
||||
|
||||
import os
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
# Client-side code #
|
||||
|
||||
|
||||
class ServerConfigTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return cherrypy.request.wsgi_environ['SERVER_PORT']
|
||||
|
||||
@cherrypy.expose
|
||||
def upload(self, file):
|
||||
return 'Size: %s' % len(file.file.read())
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'request.body.maxbytes': 100})
|
||||
def tinyupload(self):
|
||||
return cherrypy.request.body.read()
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
cherrypy.config.update({
|
||||
'server.socket_host': '0.0.0.0',
|
||||
'server.socket_port': 9876,
|
||||
'server.max_request_body_size': 200,
|
||||
'server.max_request_header_size': 500,
|
||||
'server.socket_timeout': 0.5,
|
||||
|
||||
# Test explicit server.instance
|
||||
'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
|
||||
'server.2.socket_port': 9877,
|
||||
|
||||
# Test non-numeric <servername>
|
||||
# Also test default server.instance = builtin server
|
||||
'server.yetanother.socket_port': 9878,
|
||||
})
|
||||
|
||||
PORT = 9876
|
||||
|
||||
def testBasicConfig(self):
|
||||
self.getPage('/')
|
||||
self.assertBody(str(self.PORT))
|
||||
|
||||
def testAdditionalServers(self):
|
||||
if self.scheme == 'https':
|
||||
return self.skip('not available under ssl')
|
||||
self.PORT = 9877
|
||||
self.getPage('/')
|
||||
self.assertBody(str(self.PORT))
|
||||
self.PORT = 9878
|
||||
self.getPage('/')
|
||||
self.assertBody(str(self.PORT))
|
||||
|
||||
def testMaxRequestSizePerHandler(self):
|
||||
if getattr(cherrypy.server, 'using_apache', False):
|
||||
return self.skip('skipped due to known Apache differences... ')
|
||||
|
||||
self.getPage('/tinyupload', method='POST',
|
||||
headers=[('Content-Type', 'text/plain'),
|
||||
('Content-Length', '100')],
|
||||
body='x' * 100)
|
||||
self.assertStatus(200)
|
||||
self.assertBody('x' * 100)
|
||||
|
||||
self.getPage('/tinyupload', method='POST',
|
||||
headers=[('Content-Type', 'text/plain'),
|
||||
('Content-Length', '101')],
|
||||
body='x' * 101)
|
||||
self.assertStatus(413)
|
||||
|
||||
def testMaxRequestSize(self):
|
||||
if getattr(cherrypy.server, 'using_apache', False):
|
||||
return self.skip('skipped due to known Apache differences... ')
|
||||
|
||||
for size in (500, 5000, 50000):
|
||||
self.getPage('/', headers=[('From', 'x' * 500)])
|
||||
self.assertStatus(413)
|
||||
|
||||
# Test for https://github.com/cherrypy/cherrypy/issues/421
|
||||
# (Incorrect border condition in readline of SizeCheckWrapper).
|
||||
# This hangs in rev 891 and earlier.
|
||||
lines256 = 'x' * 248
|
||||
self.getPage('/',
|
||||
headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
|
||||
('From', lines256)])
|
||||
|
||||
# Test upload
|
||||
cd = (
|
||||
'Content-Disposition: form-data; '
|
||||
'name="file"; '
|
||||
'filename="hello.txt"'
|
||||
)
|
||||
body = '\r\n'.join([
|
||||
'--x',
|
||||
cd,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'%s',
|
||||
'--x--'])
|
||||
partlen = 200 - len(body)
|
||||
b = body % ('x' * partlen)
|
||||
h = [('Content-type', 'multipart/form-data; boundary=x'),
|
||||
('Content-Length', '%s' % len(b))]
|
||||
self.getPage('/upload', h, 'POST', b)
|
||||
self.assertBody('Size: %d' % partlen)
|
||||
|
||||
b = body % ('x' * 200)
|
||||
h = [('Content-type', 'multipart/form-data; boundary=x'),
|
||||
('Content-Length', '%s' % len(b))]
|
||||
self.getPage('/upload', h, 'POST', b)
|
||||
self.assertStatus(413)
|
||||
873
lib/cherrypy/test/test_conn.py
Normal file
873
lib/cherrypy/test/test_conn.py
Normal file
@@ -0,0 +1,873 @@
|
||||
"""Tests for TCP connection handling, including proper and timely close."""
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
|
||||
|
||||
import pytest
|
||||
|
||||
from cheroot.test import webtest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import HTTPSConnection, ntob, tonative
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
timeout = 1
|
||||
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
|
||||
|
||||
|
||||
def setup_server():
|
||||
|
||||
def raise500():
|
||||
raise cherrypy.HTTPError(500)
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return pov
|
||||
page1 = index
|
||||
page2 = index
|
||||
page3 = index
|
||||
|
||||
@cherrypy.expose
|
||||
def hello(self):
|
||||
return 'Hello, world!'
|
||||
|
||||
@cherrypy.expose
|
||||
def timeout(self, t):
|
||||
return str(cherrypy.server.httpserver.timeout)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
def stream(self, set_cl=False):
|
||||
if set_cl:
|
||||
cherrypy.response.headers['Content-Length'] = 10
|
||||
|
||||
def content():
|
||||
for x in range(10):
|
||||
yield str(x)
|
||||
|
||||
return content()
|
||||
|
||||
@cherrypy.expose
|
||||
def error(self, code=500):
|
||||
raise cherrypy.HTTPError(code)
|
||||
|
||||
@cherrypy.expose
|
||||
def upload(self):
|
||||
if not cherrypy.request.method == 'POST':
|
||||
raise AssertionError("'POST' != request.method %r" %
|
||||
cherrypy.request.method)
|
||||
return "thanks for '%s'" % cherrypy.request.body.read()
|
||||
|
||||
@cherrypy.expose
|
||||
def custom(self, response_code):
|
||||
cherrypy.response.status = response_code
|
||||
return 'Code = %s' % response_code
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'hooks.on_start_resource': raise500})
|
||||
def err_before_read(self):
|
||||
return 'ok'
|
||||
|
||||
@cherrypy.expose
|
||||
def one_megabyte_of_a(self):
|
||||
return ['a' * 1024] * 1024
|
||||
|
||||
@cherrypy.expose
|
||||
# Turn off the encoding tool so it doens't collapse
|
||||
# our response body and reclaculate the Content-Length.
|
||||
@cherrypy.config(**{'tools.encode.on': False})
|
||||
def custom_cl(self, body, cl):
|
||||
cherrypy.response.headers['Content-Length'] = cl
|
||||
if not isinstance(body, list):
|
||||
body = [body]
|
||||
newbody = []
|
||||
for chunk in body:
|
||||
if isinstance(chunk, six.text_type):
|
||||
chunk = chunk.encode('ISO-8859-1')
|
||||
newbody.append(chunk)
|
||||
return newbody
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({
|
||||
'server.max_request_body_size': 1001,
|
||||
'server.socket_timeout': timeout,
|
||||
})
|
||||
|
||||
|
||||
class ConnectionCloseTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_HTTP11(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
self.persistent = True
|
||||
|
||||
# Make the first request and assert there's no "Connection: close".
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
# Make another request on the same connection.
|
||||
self.getPage('/page1')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
# Test client-side close.
|
||||
self.getPage('/page2', headers=[('Connection', 'close')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertHeader('Connection', 'close')
|
||||
|
||||
# Make another request on the same connection, which should error.
|
||||
self.assertRaises(NotConnected, self.getPage, '/')
|
||||
|
||||
def test_Streaming_no_len(self):
|
||||
try:
|
||||
self._streaming(set_cl=False)
|
||||
finally:
|
||||
try:
|
||||
self.HTTP_CONN.close()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
def test_Streaming_with_len(self):
|
||||
try:
|
||||
self._streaming(set_cl=True)
|
||||
finally:
|
||||
try:
|
||||
self.HTTP_CONN.close()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
def _streaming(self, set_cl):
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
self.persistent = True
|
||||
|
||||
# Make the first request and assert there's no "Connection: close".
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
# Make another, streamed request on the same connection.
|
||||
if set_cl:
|
||||
# When a Content-Length is provided, the content should stream
|
||||
# without closing the connection.
|
||||
self.getPage('/stream?set_cl=Yes')
|
||||
self.assertHeader('Content-Length')
|
||||
self.assertNoHeader('Connection', 'close')
|
||||
self.assertNoHeader('Transfer-Encoding')
|
||||
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('0123456789')
|
||||
else:
|
||||
# When no Content-Length response header is provided,
|
||||
# streamed output will either close the connection, or use
|
||||
# chunked encoding, to determine transfer-length.
|
||||
self.getPage('/stream')
|
||||
self.assertNoHeader('Content-Length')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('0123456789')
|
||||
|
||||
chunked_response = False
|
||||
for k, v in self.headers:
|
||||
if k.lower() == 'transfer-encoding':
|
||||
if str(v) == 'chunked':
|
||||
chunked_response = True
|
||||
|
||||
if chunked_response:
|
||||
self.assertNoHeader('Connection', 'close')
|
||||
else:
|
||||
self.assertHeader('Connection', 'close')
|
||||
|
||||
# Make another request on the same connection, which should
|
||||
# error.
|
||||
self.assertRaises(NotConnected, self.getPage, '/')
|
||||
|
||||
# Try HEAD. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/864.
|
||||
self.getPage('/stream', method='HEAD')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('')
|
||||
self.assertNoHeader('Transfer-Encoding')
|
||||
else:
|
||||
self.PROTOCOL = 'HTTP/1.0'
|
||||
|
||||
self.persistent = True
|
||||
|
||||
# Make the first request and assert Keep-Alive.
|
||||
self.getPage('/', headers=[('Connection', 'Keep-Alive')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertHeader('Connection', 'Keep-Alive')
|
||||
|
||||
# Make another, streamed request on the same connection.
|
||||
if set_cl:
|
||||
# When a Content-Length is provided, the content should
|
||||
# stream without closing the connection.
|
||||
self.getPage('/stream?set_cl=Yes',
|
||||
headers=[('Connection', 'Keep-Alive')])
|
||||
self.assertHeader('Content-Length')
|
||||
self.assertHeader('Connection', 'Keep-Alive')
|
||||
self.assertNoHeader('Transfer-Encoding')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('0123456789')
|
||||
else:
|
||||
# When a Content-Length is not provided,
|
||||
# the server should close the connection.
|
||||
self.getPage('/stream', headers=[('Connection', 'Keep-Alive')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('0123456789')
|
||||
|
||||
self.assertNoHeader('Content-Length')
|
||||
self.assertNoHeader('Connection', 'Keep-Alive')
|
||||
self.assertNoHeader('Transfer-Encoding')
|
||||
|
||||
# Make another request on the same connection, which should
|
||||
# error.
|
||||
self.assertRaises(NotConnected, self.getPage, '/')
|
||||
|
||||
def test_HTTP10_KeepAlive(self):
|
||||
self.PROTOCOL = 'HTTP/1.0'
|
||||
if self.scheme == 'https':
|
||||
self.HTTP_CONN = HTTPSConnection
|
||||
else:
|
||||
self.HTTP_CONN = HTTPConnection
|
||||
|
||||
# Test a normal HTTP/1.0 request.
|
||||
self.getPage('/page2')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
# Apache, for example, may emit a Connection header even for HTTP/1.0
|
||||
# self.assertNoHeader("Connection")
|
||||
|
||||
# Test a keep-alive HTTP/1.0 request.
|
||||
self.persistent = True
|
||||
|
||||
self.getPage('/page3', headers=[('Connection', 'Keep-Alive')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertHeader('Connection', 'Keep-Alive')
|
||||
|
||||
# Remove the keep-alive header again.
|
||||
self.getPage('/page3')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
# Apache, for example, may emit a Connection header even for HTTP/1.0
|
||||
# self.assertNoHeader("Connection")
|
||||
|
||||
|
||||
class PipelineTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_HTTP11_Timeout(self):
|
||||
# If we timeout without sending any data,
|
||||
# the server will close the conn with a 408.
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Connect but send nothing.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.auto_open = False
|
||||
conn.connect()
|
||||
|
||||
# Wait for our socket timeout
|
||||
time.sleep(timeout * 2)
|
||||
|
||||
# The request should have returned 408 already.
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 408)
|
||||
conn.close()
|
||||
|
||||
# Connect but send half the headers only.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.auto_open = False
|
||||
conn.connect()
|
||||
conn.send(b'GET /hello HTTP/1.1')
|
||||
conn.send(('Host: %s' % self.HOST).encode('ascii'))
|
||||
|
||||
# Wait for our socket timeout
|
||||
time.sleep(timeout * 2)
|
||||
|
||||
# The conn should have already sent 408.
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 408)
|
||||
conn.close()
|
||||
|
||||
def test_HTTP11_Timeout_after_request(self):
|
||||
# If we timeout after at least one request has succeeded,
|
||||
# the server will close the conn without 408.
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Make an initial request
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.body = response.read()
|
||||
self.assertBody(str(timeout))
|
||||
|
||||
# Make a second request on the same socket
|
||||
conn._output(b'GET /hello HTTP/1.1')
|
||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||
conn._send_output()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.body = response.read()
|
||||
self.assertBody('Hello, world!')
|
||||
|
||||
# Wait for our socket timeout
|
||||
time.sleep(timeout * 2)
|
||||
|
||||
# Make another request on the same socket, which should error
|
||||
conn._output(b'GET /hello HTTP/1.1')
|
||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||
conn._send_output()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
try:
|
||||
response.begin()
|
||||
except Exception:
|
||||
if not isinstance(sys.exc_info()[1],
|
||||
(socket.error, BadStatusLine)):
|
||||
self.fail("Writing to timed out socket didn't fail"
|
||||
' as it should have: %s' % sys.exc_info()[1])
|
||||
else:
|
||||
if response.status != 408:
|
||||
self.fail("Writing to timed out socket didn't fail"
|
||||
' as it should have: %s' %
|
||||
response.read())
|
||||
|
||||
conn.close()
|
||||
|
||||
# Make another request on a new socket, which should work
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.body = response.read()
|
||||
self.assertBody(pov)
|
||||
|
||||
# Make another request on the same socket,
|
||||
# but timeout on the headers
|
||||
conn.send(b'GET /hello HTTP/1.1')
|
||||
# Wait for our socket timeout
|
||||
time.sleep(timeout * 2)
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
try:
|
||||
response.begin()
|
||||
except Exception:
|
||||
if not isinstance(sys.exc_info()[1],
|
||||
(socket.error, BadStatusLine)):
|
||||
self.fail("Writing to timed out socket didn't fail"
|
||||
' as it should have: %s' % sys.exc_info()[1])
|
||||
else:
|
||||
self.fail("Writing to timed out socket didn't fail"
|
||||
' as it should have: %s' %
|
||||
response.read())
|
||||
|
||||
conn.close()
|
||||
|
||||
# Retry the request on a new connection, which should work
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.body = response.read()
|
||||
self.assertBody(pov)
|
||||
conn.close()
|
||||
|
||||
def test_HTTP11_pipelining(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Test pipelining. httplib doesn't support this directly.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
|
||||
# Put request 1
|
||||
conn.putrequest('GET', '/hello', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
|
||||
for trial in range(5):
|
||||
# Put next request
|
||||
conn._output(b'GET /hello HTTP/1.1')
|
||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||
conn._send_output()
|
||||
|
||||
# Retrieve previous response
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
# there is a bug in python3 regarding the buffering of
|
||||
# ``conn.sock``. Until that bug get's fixed we will
|
||||
# monkey patch the ``response`` instance.
|
||||
# https://bugs.python.org/issue23377
|
||||
if six.PY3:
|
||||
response.fp = conn.sock.makefile('rb', 0)
|
||||
response.begin()
|
||||
body = response.read(13)
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, b'Hello, world!')
|
||||
|
||||
# Retrieve final response
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
body = response.read()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, b'Hello, world!')
|
||||
|
||||
conn.close()
|
||||
|
||||
def test_100_Continue(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
|
||||
# Try a page without an Expect request header first.
|
||||
# Note that httplib's response.begin automatically ignores
|
||||
# 100 Continue responses, so we must manually check for it.
|
||||
try:
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
conn.putheader('Content-Length', '4')
|
||||
conn.endheaders()
|
||||
conn.send(ntob("d'oh"))
|
||||
response = conn.response_class(conn.sock, method='POST')
|
||||
version, status, reason = response._read_status()
|
||||
self.assertNotEqual(status, 100)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Now try a page with an Expect header...
|
||||
try:
|
||||
conn.connect()
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
conn.putheader('Content-Length', '17')
|
||||
conn.putheader('Expect', '100-continue')
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='POST')
|
||||
|
||||
# ...assert and then skip the 100 response
|
||||
version, status, reason = response._read_status()
|
||||
self.assertEqual(status, 100)
|
||||
while True:
|
||||
line = response.fp.readline().strip()
|
||||
if line:
|
||||
self.fail(
|
||||
'100 Continue should not output any headers. Got %r' %
|
||||
line)
|
||||
else:
|
||||
break
|
||||
|
||||
# ...send the body
|
||||
body = b'I am a small file'
|
||||
conn.send(body)
|
||||
|
||||
# ...get the final response
|
||||
response.begin()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(200)
|
||||
self.assertBody("thanks for '%s'" % body)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class ConnectionTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_readall_or_close(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
if self.scheme == 'https':
|
||||
self.HTTP_CONN = HTTPSConnection
|
||||
else:
|
||||
self.HTTP_CONN = HTTPConnection
|
||||
|
||||
# Test a max of 0 (the default) and then reset to what it was above.
|
||||
old_max = cherrypy.server.max_request_body_size
|
||||
for new_max in (0, old_max):
|
||||
cherrypy.server.max_request_body_size = new_max
|
||||
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
|
||||
# Get a POST page with an error
|
||||
conn.putrequest('POST', '/err_before_read', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
conn.putheader('Content-Length', '1000')
|
||||
conn.putheader('Expect', '100-continue')
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='POST')
|
||||
|
||||
# ...assert and then skip the 100 response
|
||||
version, status, reason = response._read_status()
|
||||
self.assertEqual(status, 100)
|
||||
while True:
|
||||
skip = response.fp.readline().strip()
|
||||
if not skip:
|
||||
break
|
||||
|
||||
# ...send the body
|
||||
conn.send(ntob('x' * 1000))
|
||||
|
||||
# ...get the final response
|
||||
response.begin()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(500)
|
||||
|
||||
# Now try a working page with an Expect header...
|
||||
conn._output(b'POST /upload HTTP/1.1')
|
||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||
conn._output(b'Content-Type: text/plain')
|
||||
conn._output(b'Content-Length: 17')
|
||||
conn._output(b'Expect: 100-continue')
|
||||
conn._send_output()
|
||||
response = conn.response_class(conn.sock, method='POST')
|
||||
|
||||
# ...assert and then skip the 100 response
|
||||
version, status, reason = response._read_status()
|
||||
self.assertEqual(status, 100)
|
||||
while True:
|
||||
skip = response.fp.readline().strip()
|
||||
if not skip:
|
||||
break
|
||||
|
||||
# ...send the body
|
||||
body = b'I am a small file'
|
||||
conn.send(body)
|
||||
|
||||
# ...get the final response
|
||||
response.begin()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(200)
|
||||
self.assertBody("thanks for '%s'" % body)
|
||||
conn.close()
|
||||
|
||||
def test_No_Message_Body(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Set our HTTP_CONN to an instance so it persists between requests.
|
||||
self.persistent = True
|
||||
|
||||
# Make the first request and assert there's no "Connection: close".
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody(pov)
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
# Make a 204 request on the same connection.
|
||||
self.getPage('/custom/204')
|
||||
self.assertStatus(204)
|
||||
self.assertNoHeader('Content-Length')
|
||||
self.assertBody('')
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
# Make a 304 request on the same connection.
|
||||
self.getPage('/custom/304')
|
||||
self.assertStatus(304)
|
||||
self.assertNoHeader('Content-Length')
|
||||
self.assertBody('')
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
def test_Chunked_Encoding(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
if (hasattr(self, 'harness') and
|
||||
'modpython' in self.harness.__class__.__name__.lower()):
|
||||
# mod_python forbids chunked encoding
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Set our HTTP_CONN to an instance so it persists between requests.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
|
||||
# Try a normal chunked request (with extensions)
|
||||
body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
|
||||
'Content-Type: application/json\r\n'
|
||||
'\r\n')
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Transfer-Encoding', 'chunked')
|
||||
conn.putheader('Trailer', 'Content-Type')
|
||||
# Note that this is somewhat malformed:
|
||||
# we shouldn't be sending Content-Length.
|
||||
# RFC 2616 says the server should ignore it.
|
||||
conn.putheader('Content-Length', '3')
|
||||
conn.endheaders()
|
||||
conn.send(body)
|
||||
response = conn.getresponse()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy')
|
||||
|
||||
# Try a chunked request that exceeds server.max_request_body_size.
|
||||
# Note that the delimiters and trailer are included.
|
||||
body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n')
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Transfer-Encoding', 'chunked')
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
# Chunked requests don't need a content-length
|
||||
# # conn.putheader("Content-Length", len(body))
|
||||
conn.endheaders()
|
||||
conn.send(body)
|
||||
response = conn.getresponse()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(413)
|
||||
conn.close()
|
||||
|
||||
def test_Content_Length_in(self):
|
||||
# Try a non-chunked request where Content-Length exceeds
|
||||
# server.max_request_body_size. Assert error before body send.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
conn.putheader('Content-Length', '9999')
|
||||
conn.endheaders()
|
||||
response = conn.getresponse()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(413)
|
||||
self.assertBody('The entity sent with the request exceeds '
|
||||
'the maximum allowed bytes.')
|
||||
conn.close()
|
||||
|
||||
def test_Content_Length_out_preheaders(self):
|
||||
# Try a non-chunked response where Content-Length is less than
|
||||
# the actual bytes in the response body.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5',
|
||||
skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.getresponse()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(500)
|
||||
self.assertBody(
|
||||
'The requested resource returned more bytes than the '
|
||||
'declared Content-Length.')
|
||||
conn.close()
|
||||
|
||||
def test_Content_Length_out_postheaders(self):
|
||||
# Try a non-chunked response where Content-Length is less than
|
||||
# the actual bytes in the response body.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest(
|
||||
'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5',
|
||||
skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.getresponse()
|
||||
self.status, self.headers, self.body = webtest.shb(response)
|
||||
self.assertStatus(200)
|
||||
self.assertBody('I too')
|
||||
conn.close()
|
||||
|
||||
def test_598(self):
|
||||
tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/'
|
||||
url = tmpl.format(
|
||||
scheme=self.scheme,
|
||||
host=self.HOST,
|
||||
port=self.PORT,
|
||||
)
|
||||
remote_data_conn = urllib.request.urlopen(url)
|
||||
buf = remote_data_conn.read(512)
|
||||
time.sleep(timeout * 0.6)
|
||||
remaining = (1024 * 1024) - 512
|
||||
while remaining:
|
||||
data = remote_data_conn.read(remaining)
|
||||
if not data:
|
||||
break
|
||||
else:
|
||||
buf += data
|
||||
remaining -= len(data)
|
||||
|
||||
self.assertEqual(len(buf), 1024 * 1024)
|
||||
self.assertEqual(buf, ntob('a' * 1024 * 1024))
|
||||
self.assertEqual(remaining, 0)
|
||||
remote_data_conn.close()
|
||||
|
||||
|
||||
def setup_upload_server():
|
||||
|
||||
class Root:
|
||||
@cherrypy.expose
|
||||
def upload(self):
|
||||
if not cherrypy.request.method == 'POST':
|
||||
raise AssertionError("'POST' != request.method %r" %
|
||||
cherrypy.request.method)
|
||||
return "thanks for '%s'" % tonative(cherrypy.request.body.read())
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({
|
||||
'server.max_request_body_size': 1001,
|
||||
'server.socket_timeout': 10,
|
||||
'server.accepted_queue_size': 5,
|
||||
'server.accepted_queue_timeout': 0.1,
|
||||
})
|
||||
|
||||
|
||||
reset_names = 'ECONNRESET', 'WSAECONNRESET'
|
||||
socket_reset_errors = [
|
||||
getattr(errno, name)
|
||||
for name in reset_names
|
||||
if hasattr(errno, name)
|
||||
]
|
||||
'reset error numbers available on this platform'
|
||||
|
||||
socket_reset_errors += [
|
||||
# Python 3.5 raises an http.client.RemoteDisconnected
|
||||
# with this message
|
||||
'Remote end closed connection without response',
|
||||
]
|
||||
|
||||
|
||||
class LimitedRequestQueueTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_upload_server)
|
||||
|
||||
@pytest.mark.xfail(reason='#1535')
|
||||
def test_queue_full(self):
|
||||
conns = []
|
||||
overflow_conn = None
|
||||
|
||||
try:
|
||||
# Make 15 initial requests and leave them open, which should use
|
||||
# all of wsgiserver's WorkerThreads and fill its Queue.
|
||||
for i in range(15):
|
||||
conn = self.HTTP_CONN(self.HOST, self.PORT)
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.putheader('Content-Type', 'text/plain')
|
||||
conn.putheader('Content-Length', '4')
|
||||
conn.endheaders()
|
||||
conns.append(conn)
|
||||
|
||||
# Now try a 16th conn, which should be closed by the
|
||||
# server immediately.
|
||||
overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
|
||||
# Manually connect since httplib won't let us set a timeout
|
||||
for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
|
||||
socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
overflow_conn.sock = socket.socket(af, socktype, proto)
|
||||
overflow_conn.sock.settimeout(5)
|
||||
overflow_conn.sock.connect(sa)
|
||||
break
|
||||
|
||||
overflow_conn.putrequest('GET', '/', skip_host=True)
|
||||
overflow_conn.putheader('Host', self.HOST)
|
||||
overflow_conn.endheaders()
|
||||
response = overflow_conn.response_class(
|
||||
overflow_conn.sock,
|
||||
method='GET',
|
||||
)
|
||||
try:
|
||||
response.begin()
|
||||
except socket.error as exc:
|
||||
if exc.args[0] in socket_reset_errors:
|
||||
pass # Expected.
|
||||
else:
|
||||
tmpl = (
|
||||
'Overflow conn did not get RST. '
|
||||
'Got {exc.args!r} instead'
|
||||
)
|
||||
raise AssertionError(tmpl.format(**locals()))
|
||||
except BadStatusLine:
|
||||
# This is a special case in OS X. Linux and Windows will
|
||||
# RST correctly.
|
||||
assert sys.platform == 'darwin'
|
||||
else:
|
||||
raise AssertionError('Overflow conn did not get RST ')
|
||||
finally:
|
||||
for conn in conns:
|
||||
conn.send(b'done')
|
||||
response = conn.response_class(conn.sock, method='POST')
|
||||
response.begin()
|
||||
self.body = response.read()
|
||||
self.assertBody("thanks for 'done'")
|
||||
self.assertEqual(response.status, 200)
|
||||
conn.close()
|
||||
if overflow_conn:
|
||||
overflow_conn.close()
|
||||
|
||||
|
||||
class BadRequestTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_No_CRLF(self):
|
||||
self.persistent = True
|
||||
|
||||
conn = self.HTTP_CONN
|
||||
conn.send(b'GET /hello HTTP/1.1\n\n')
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.body = response.read()
|
||||
self.assertBody('HTTP requires CRLF terminators')
|
||||
conn.close()
|
||||
|
||||
conn.connect()
|
||||
conn.send(b'GET /hello HTTP/1.1\r\n\n')
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.body = response.read()
|
||||
self.assertBody('HTTP requires CRLF terminators')
|
||||
conn.close()
|
||||
823
lib/cherrypy/test/test_core.py
Normal file
823
lib/cherrypy/test/test_core.py
Normal file
@@ -0,0 +1,823 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""Basic tests for the CherryPy core: request handling."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy import _cptools, tools
|
||||
from cherrypy.lib import httputil, static
|
||||
|
||||
from cherrypy.test._test_decorators import ExposeExamples
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
localDir = os.path.dirname(__file__)
|
||||
favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico')
|
||||
|
||||
# Client-side code #
|
||||
|
||||
|
||||
class CoreRequestHandlingTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'hello'
|
||||
|
||||
favicon_ico = tools.staticfile.handler(filename=favicon_path)
|
||||
|
||||
@cherrypy.expose
|
||||
def defct(self, newct):
|
||||
newct = 'text/%s' % newct
|
||||
cherrypy.config.update({'tools.response_headers.on': True,
|
||||
'tools.response_headers.headers':
|
||||
[('Content-Type', newct)]})
|
||||
|
||||
@cherrypy.expose
|
||||
def baseurl(self, path_info, relative=None):
|
||||
return cherrypy.url(path_info, relative=bool(relative))
|
||||
|
||||
root = Root()
|
||||
root.expose_dec = ExposeExamples()
|
||||
|
||||
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 six.itervalues(dct):
|
||||
if isinstance(value, types.FunctionType):
|
||||
value.exposed = True
|
||||
setattr(root, name.lower(), cls())
|
||||
Test = TestType('Test', (object, ), {})
|
||||
|
||||
@cherrypy.config(**{'tools.trailing_slash.on': False})
|
||||
class URL(Test):
|
||||
|
||||
def index(self, path_info, relative=None):
|
||||
if relative != 'server':
|
||||
relative = bool(relative)
|
||||
return cherrypy.url(path_info, relative=relative)
|
||||
|
||||
def leaf(self, path_info, relative=None):
|
||||
if relative != 'server':
|
||||
relative = bool(relative)
|
||||
return cherrypy.url(path_info, relative=relative)
|
||||
|
||||
def qs(self, qs):
|
||||
return cherrypy.url(qs=qs)
|
||||
|
||||
def log_status():
|
||||
Status.statuses.append(cherrypy.response.status)
|
||||
cherrypy.tools.log_status = cherrypy.Tool(
|
||||
'on_end_resource', log_status)
|
||||
|
||||
class Status(Test):
|
||||
|
||||
def index(self):
|
||||
return 'normal'
|
||||
|
||||
def blank(self):
|
||||
cherrypy.response.status = ''
|
||||
|
||||
# According to RFC 2616, new status codes are OK as long as they
|
||||
# are between 100 and 599.
|
||||
|
||||
# Here is an illegal code...
|
||||
def illegal(self):
|
||||
cherrypy.response.status = 781
|
||||
return 'oops'
|
||||
|
||||
# ...and here is an unknown but legal code.
|
||||
def unknown(self):
|
||||
cherrypy.response.status = '431 My custom error'
|
||||
return 'funky'
|
||||
|
||||
# Non-numeric code
|
||||
def bad(self):
|
||||
cherrypy.response.status = 'error'
|
||||
return 'bad news'
|
||||
|
||||
statuses = []
|
||||
|
||||
@cherrypy.config(**{'tools.log_status.on': True})
|
||||
def on_end_resource_stage(self):
|
||||
return repr(self.statuses)
|
||||
|
||||
class Redirect(Test):
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.err_redirect.on': True,
|
||||
'tools.err_redirect.url': '/errpage',
|
||||
'tools.err_redirect.internal': False,
|
||||
})
|
||||
class Error:
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise NameError('redirect_test')
|
||||
|
||||
error = Error()
|
||||
|
||||
def index(self):
|
||||
return 'child'
|
||||
|
||||
def custom(self, url, code):
|
||||
raise cherrypy.HTTPRedirect(url, code)
|
||||
|
||||
@cherrypy.config(**{'tools.trailing_slash.extra': True})
|
||||
def by_code(self, code):
|
||||
raise cherrypy.HTTPRedirect('somewhere%20else', code)
|
||||
|
||||
def nomodify(self):
|
||||
raise cherrypy.HTTPRedirect('', 304)
|
||||
|
||||
def proxy(self):
|
||||
raise cherrypy.HTTPRedirect('proxy', 305)
|
||||
|
||||
def stringify(self):
|
||||
return str(cherrypy.HTTPRedirect('/'))
|
||||
|
||||
def fragment(self, frag):
|
||||
raise cherrypy.HTTPRedirect('/some/url#%s' % frag)
|
||||
|
||||
def url_with_quote(self):
|
||||
raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
|
||||
|
||||
def url_with_xss(self):
|
||||
raise cherrypy.HTTPRedirect(
|
||||
"/some<script>alert(1);</script>url/that'we/want")
|
||||
|
||||
def url_with_unicode(self):
|
||||
raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8'))
|
||||
|
||||
def login_redir():
|
||||
if not getattr(cherrypy.request, 'login', None):
|
||||
raise cherrypy.InternalRedirect('/internalredirect/login')
|
||||
tools.login_redir = _cptools.Tool('before_handler', login_redir)
|
||||
|
||||
def redir_custom():
|
||||
raise cherrypy.InternalRedirect('/internalredirect/custom_err')
|
||||
|
||||
class InternalRedirect(Test):
|
||||
|
||||
def index(self):
|
||||
raise cherrypy.InternalRedirect('/')
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'hooks.before_error_response': redir_custom})
|
||||
def choke(self):
|
||||
return 3 / 0
|
||||
|
||||
def relative(self, a, b):
|
||||
raise cherrypy.InternalRedirect('cousin?t=6')
|
||||
|
||||
def cousin(self, t):
|
||||
assert cherrypy.request.prev.closed
|
||||
return cherrypy.request.prev.query_string
|
||||
|
||||
def petshop(self, user_id):
|
||||
if user_id == 'parrot':
|
||||
# Trade it for a slug when redirecting
|
||||
raise cherrypy.InternalRedirect(
|
||||
'/image/getImagesByUser?user_id=slug')
|
||||
elif user_id == 'terrier':
|
||||
# Trade it for a fish when redirecting
|
||||
raise cherrypy.InternalRedirect(
|
||||
'/image/getImagesByUser?user_id=fish')
|
||||
else:
|
||||
# This should pass the user_id through to getImagesByUser
|
||||
raise cherrypy.InternalRedirect(
|
||||
'/image/getImagesByUser?user_id=%s' % str(user_id))
|
||||
|
||||
# We support Python 2.3, but the @-deco syntax would look like
|
||||
# this:
|
||||
# @tools.login_redir()
|
||||
def secure(self):
|
||||
return 'Welcome!'
|
||||
secure = tools.login_redir()(secure)
|
||||
# Since calling the tool returns the same function you pass in,
|
||||
# you could skip binding the return value, and just write:
|
||||
# tools.login_redir()(secure)
|
||||
|
||||
def login(self):
|
||||
return 'Please log in'
|
||||
|
||||
def custom_err(self):
|
||||
return 'Something went horribly wrong.'
|
||||
|
||||
@cherrypy.config(**{'hooks.before_request_body': redir_custom})
|
||||
def early_ir(self, arg):
|
||||
return 'whatever'
|
||||
|
||||
class Image(Test):
|
||||
|
||||
def getImagesByUser(self, user_id):
|
||||
return '0 images for %s' % user_id
|
||||
|
||||
class Flatten(Test):
|
||||
|
||||
def as_string(self):
|
||||
return 'content'
|
||||
|
||||
def as_list(self):
|
||||
return ['con', 'tent']
|
||||
|
||||
def as_yield(self):
|
||||
yield b'content'
|
||||
|
||||
@cherrypy.config(**{'tools.flatten.on': True})
|
||||
def as_dblyield(self):
|
||||
yield self.as_yield()
|
||||
|
||||
def as_refyield(self):
|
||||
for chunk in self.as_yield():
|
||||
yield chunk
|
||||
|
||||
class Ranges(Test):
|
||||
|
||||
def get_ranges(self, bytes):
|
||||
return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
|
||||
|
||||
def slice_file(self):
|
||||
path = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
return static.serve_file(
|
||||
os.path.join(path, 'static/index.html'))
|
||||
|
||||
class Cookies(Test):
|
||||
|
||||
def single(self, name):
|
||||
cookie = cherrypy.request.cookie[name]
|
||||
# Python2's SimpleCookie.__setitem__ won't take unicode keys.
|
||||
cherrypy.response.cookie[str(name)] = cookie.value
|
||||
|
||||
def multiple(self, names):
|
||||
list(map(self.single, names))
|
||||
|
||||
def append_headers(header_list, debug=False):
|
||||
if debug:
|
||||
cherrypy.log(
|
||||
'Extending response headers with %s' % repr(header_list),
|
||||
'TOOLS.APPEND_HEADERS')
|
||||
cherrypy.serving.response.header_list.extend(header_list)
|
||||
cherrypy.tools.append_headers = cherrypy.Tool(
|
||||
'on_end_resource', append_headers)
|
||||
|
||||
class MultiHeader(Test):
|
||||
|
||||
def header_list(self):
|
||||
pass
|
||||
header_list = cherrypy.tools.append_headers(header_list=[
|
||||
(b'WWW-Authenticate', b'Negotiate'),
|
||||
(b'WWW-Authenticate', b'Basic realm="foo"'),
|
||||
])(header_list)
|
||||
|
||||
def commas(self):
|
||||
cherrypy.response.headers[
|
||||
'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
|
||||
|
||||
cherrypy.tree.mount(root)
|
||||
|
||||
def testStatus(self):
|
||||
self.getPage('/status/')
|
||||
self.assertBody('normal')
|
||||
self.assertStatus(200)
|
||||
|
||||
self.getPage('/status/blank')
|
||||
self.assertBody('')
|
||||
self.assertStatus(200)
|
||||
|
||||
self.getPage('/status/illegal')
|
||||
self.assertStatus(500)
|
||||
msg = 'Illegal response status from server (781 is out of range).'
|
||||
self.assertErrorPage(500, msg)
|
||||
|
||||
if not getattr(cherrypy.server, 'using_apache', False):
|
||||
self.getPage('/status/unknown')
|
||||
self.assertBody('funky')
|
||||
self.assertStatus(431)
|
||||
|
||||
self.getPage('/status/bad')
|
||||
self.assertStatus(500)
|
||||
msg = "Illegal response status from server ('error' is non-numeric)."
|
||||
self.assertErrorPage(500, msg)
|
||||
|
||||
def test_on_end_resource_status(self):
|
||||
self.getPage('/status/on_end_resource_stage')
|
||||
self.assertBody('[]')
|
||||
self.getPage('/status/on_end_resource_stage')
|
||||
self.assertBody(repr(['200 OK']))
|
||||
|
||||
def testSlashes(self):
|
||||
# Test that requests for index methods without a trailing slash
|
||||
# get redirected to the same URI path with a trailing slash.
|
||||
# Make sure GET params are preserved.
|
||||
self.getPage('/redirect?id=3')
|
||||
self.assertStatus(301)
|
||||
self.assertMatchesBody(
|
||||
'<a href=([\'"])%s/redirect/[?]id=3\\1>'
|
||||
'%s/redirect/[?]id=3</a>' % (self.base(), self.base())
|
||||
)
|
||||
|
||||
if self.prefix():
|
||||
# Corner case: the "trailing slash" redirect could be tricky if
|
||||
# we're using a virtual root and the URI is "/vroot" (no slash).
|
||||
self.getPage('')
|
||||
self.assertStatus(301)
|
||||
self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
|
||||
(self.base(), self.base()))
|
||||
|
||||
# Test that requests for NON-index methods WITH a trailing slash
|
||||
# get redirected to the same URI path WITHOUT a trailing slash.
|
||||
# Make sure GET params are preserved.
|
||||
self.getPage('/redirect/by_code/?code=307')
|
||||
self.assertStatus(301)
|
||||
self.assertMatchesBody(
|
||||
"<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
|
||||
'%s/redirect/by_code[?]code=307</a>'
|
||||
% (self.base(), self.base())
|
||||
)
|
||||
|
||||
# If the trailing_slash tool is off, CP should just continue
|
||||
# as if the slashes were correct. But it needs some help
|
||||
# inside cherrypy.url to form correct output.
|
||||
self.getPage('/url?path_info=page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
self.getPage('/url/leaf/?path_info=page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
|
||||
def testRedirect(self):
|
||||
self.getPage('/redirect/')
|
||||
self.assertBody('child')
|
||||
self.assertStatus(200)
|
||||
|
||||
self.getPage('/redirect/by_code?code=300')
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||
self.assertStatus(300)
|
||||
|
||||
self.getPage('/redirect/by_code?code=301')
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||
self.assertStatus(301)
|
||||
|
||||
self.getPage('/redirect/by_code?code=302')
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||
self.assertStatus(302)
|
||||
|
||||
self.getPage('/redirect/by_code?code=303')
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||
self.assertStatus(303)
|
||||
|
||||
self.getPage('/redirect/by_code?code=307')
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||
self.assertStatus(307)
|
||||
|
||||
self.getPage('/redirect/nomodify')
|
||||
self.assertBody('')
|
||||
self.assertStatus(304)
|
||||
|
||||
self.getPage('/redirect/proxy')
|
||||
self.assertBody('')
|
||||
self.assertStatus(305)
|
||||
|
||||
# HTTPRedirect on error
|
||||
self.getPage('/redirect/error/')
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
self.assertInBody('/errpage')
|
||||
|
||||
# Make sure str(HTTPRedirect()) works.
|
||||
self.getPage('/redirect/stringify', protocol='HTTP/1.0')
|
||||
self.assertStatus(200)
|
||||
self.assertBody("(['%s/'], 302)" % self.base())
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.getPage('/redirect/stringify', protocol='HTTP/1.1')
|
||||
self.assertStatus(200)
|
||||
self.assertBody("(['%s/'], 303)" % self.base())
|
||||
|
||||
# check that #fragments are handled properly
|
||||
# http://skrb.org/ietf/http_errata.html#location-fragments
|
||||
frag = 'foo'
|
||||
self.getPage('/redirect/fragment/%s' % frag)
|
||||
self.assertMatchesBody(
|
||||
r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
|
||||
frag, frag))
|
||||
loc = self.assertHeader('Location')
|
||||
assert loc.endswith('#%s' % frag)
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
|
||||
# check injection protection
|
||||
# See https://github.com/cherrypy/cherrypy/issues/1003
|
||||
self.getPage(
|
||||
'/redirect/custom?'
|
||||
'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval')
|
||||
self.assertStatus(303)
|
||||
loc = self.assertHeader('Location')
|
||||
assert 'Set-Cookie' in loc
|
||||
self.assertNoHeader('Set-Cookie')
|
||||
|
||||
def assertValidXHTML():
|
||||
from xml.etree import ElementTree
|
||||
try:
|
||||
ElementTree.fromstring(
|
||||
'<html><body>%s</body></html>' % self.body,
|
||||
)
|
||||
except ElementTree.ParseError:
|
||||
self._handlewebError(
|
||||
'automatically generated redirect did not '
|
||||
'generate well-formed html',
|
||||
)
|
||||
|
||||
# check redirects to URLs generated valid HTML - we check this
|
||||
# by seeing if it appears as valid XHTML.
|
||||
self.getPage('/redirect/by_code?code=303')
|
||||
self.assertStatus(303)
|
||||
assertValidXHTML()
|
||||
|
||||
# do the same with a url containing quote characters.
|
||||
self.getPage('/redirect/url_with_quote')
|
||||
self.assertStatus(303)
|
||||
assertValidXHTML()
|
||||
|
||||
def test_redirect_with_xss(self):
|
||||
"""A redirect to a URL with HTML injected should result
|
||||
in page contents escaped."""
|
||||
self.getPage('/redirect/url_with_xss')
|
||||
self.assertStatus(303)
|
||||
assert b'<script>' not in self.body
|
||||
assert b'<script>' in self.body
|
||||
|
||||
def test_redirect_with_unicode(self):
|
||||
"""
|
||||
A redirect to a URL with Unicode should return a Location
|
||||
header containing that Unicode URL.
|
||||
"""
|
||||
# test disabled due to #1440
|
||||
return
|
||||
self.getPage('/redirect/url_with_unicode')
|
||||
self.assertStatus(303)
|
||||
loc = self.assertHeader('Location')
|
||||
assert ntou('тест', encoding='utf-8') in loc
|
||||
|
||||
def test_InternalRedirect(self):
|
||||
# InternalRedirect
|
||||
self.getPage('/internalredirect/')
|
||||
self.assertBody('hello')
|
||||
self.assertStatus(200)
|
||||
|
||||
# Test passthrough
|
||||
self.getPage(
|
||||
'/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film')
|
||||
self.assertBody('0 images for Sir-not-appearing-in-this-film')
|
||||
self.assertStatus(200)
|
||||
|
||||
# Test args
|
||||
self.getPage('/internalredirect/petshop?user_id=parrot')
|
||||
self.assertBody('0 images for slug')
|
||||
self.assertStatus(200)
|
||||
|
||||
# Test POST
|
||||
self.getPage('/internalredirect/petshop', method='POST',
|
||||
body='user_id=terrier')
|
||||
self.assertBody('0 images for fish')
|
||||
self.assertStatus(200)
|
||||
|
||||
# Test ir before body read
|
||||
self.getPage('/internalredirect/early_ir', method='POST',
|
||||
body='arg=aha!')
|
||||
self.assertBody('Something went horribly wrong.')
|
||||
self.assertStatus(200)
|
||||
|
||||
self.getPage('/internalredirect/secure')
|
||||
self.assertBody('Please log in')
|
||||
self.assertStatus(200)
|
||||
|
||||
# Relative path in InternalRedirect.
|
||||
# Also tests request.prev.
|
||||
self.getPage('/internalredirect/relative?a=3&b=5')
|
||||
self.assertBody('a=3&b=5')
|
||||
self.assertStatus(200)
|
||||
|
||||
# InternalRedirect on error
|
||||
self.getPage('/internalredirect/choke')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Something went horribly wrong.')
|
||||
|
||||
def testFlatten(self):
|
||||
for url in ['/flatten/as_string', '/flatten/as_list',
|
||||
'/flatten/as_yield', '/flatten/as_dblyield',
|
||||
'/flatten/as_refyield']:
|
||||
self.getPage(url)
|
||||
self.assertBody('content')
|
||||
|
||||
def testRanges(self):
|
||||
self.getPage('/ranges/get_ranges?bytes=3-6')
|
||||
self.assertBody('[(3, 7)]')
|
||||
|
||||
# Test multiple ranges and a suffix-byte-range-spec, for good measure.
|
||||
self.getPage('/ranges/get_ranges?bytes=2-4,-1')
|
||||
self.assertBody('[(2, 5), (7, 8)]')
|
||||
|
||||
# Test a suffix-byte-range longer than the content
|
||||
# length. Note that in this test, the content length
|
||||
# is 8 bytes.
|
||||
self.getPage('/ranges/get_ranges?bytes=-100')
|
||||
self.assertBody('[(0, 8)]')
|
||||
|
||||
# Get a partial file.
|
||||
if cherrypy.server.protocol_version == 'HTTP/1.1':
|
||||
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
|
||||
self.assertStatus(206)
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
self.assertHeader('Content-Range', 'bytes 2-5/14')
|
||||
self.assertBody('llo,')
|
||||
|
||||
# What happens with overlapping ranges (and out of order, too)?
|
||||
self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')])
|
||||
self.assertStatus(206)
|
||||
ct = self.assertHeader('Content-Type')
|
||||
expected_type = 'multipart/byteranges; boundary='
|
||||
self.assert_(ct.startswith(expected_type))
|
||||
boundary = ct[len(expected_type):]
|
||||
expected_body = ('\r\n--%s\r\n'
|
||||
'Content-type: text/html\r\n'
|
||||
'Content-range: bytes 4-6/14\r\n'
|
||||
'\r\n'
|
||||
'o, \r\n'
|
||||
'--%s\r\n'
|
||||
'Content-type: text/html\r\n'
|
||||
'Content-range: bytes 2-5/14\r\n'
|
||||
'\r\n'
|
||||
'llo,\r\n'
|
||||
'--%s--\r\n' % (boundary, boundary, boundary))
|
||||
self.assertBody(expected_body)
|
||||
self.assertHeader('Content-Length')
|
||||
|
||||
# Test "416 Requested Range Not Satisfiable"
|
||||
self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')])
|
||||
self.assertStatus(416)
|
||||
# "When this status code is returned for a byte-range request,
|
||||
# the response SHOULD include a Content-Range entity-header
|
||||
# field specifying the current length of the selected resource"
|
||||
self.assertHeader('Content-Range', 'bytes */14')
|
||||
elif cherrypy.server.protocol_version == 'HTTP/1.0':
|
||||
# Test Range behavior with HTTP/1.0 request
|
||||
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Hello, world\r\n')
|
||||
|
||||
def testFavicon(self):
|
||||
# favicon.ico is served by staticfile.
|
||||
icofilename = os.path.join(localDir, '../favicon.ico')
|
||||
icofile = open(icofilename, 'rb')
|
||||
data = icofile.read()
|
||||
icofile.close()
|
||||
|
||||
self.getPage('/favicon.ico')
|
||||
self.assertBody(data)
|
||||
|
||||
def skip_if_bad_cookies(self):
|
||||
"""
|
||||
cookies module fails to reject invalid cookies
|
||||
https://github.com/cherrypy/cherrypy/issues/1405
|
||||
"""
|
||||
cookies = sys.modules.get('http.cookies')
|
||||
_is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
|
||||
if not _is_legal_key(','):
|
||||
return
|
||||
issue = 'http://bugs.python.org/issue26302'
|
||||
tmpl = 'Broken cookies module ({issue})'
|
||||
self.skip(tmpl.format(**locals()))
|
||||
|
||||
def testCookies(self):
|
||||
self.skip_if_bad_cookies()
|
||||
|
||||
self.getPage('/cookies/single?name=First',
|
||||
[('Cookie', 'First=Dinsdale;')])
|
||||
self.assertHeader('Set-Cookie', 'First=Dinsdale')
|
||||
|
||||
self.getPage('/cookies/multiple?names=First&names=Last',
|
||||
[('Cookie', 'First=Dinsdale; Last=Piranha;'),
|
||||
])
|
||||
self.assertHeader('Set-Cookie', 'First=Dinsdale')
|
||||
self.assertHeader('Set-Cookie', 'Last=Piranha')
|
||||
|
||||
self.getPage('/cookies/single?name=Something-With%2CComma',
|
||||
[('Cookie', 'Something-With,Comma=some-value')])
|
||||
self.assertStatus(400)
|
||||
|
||||
def testDefaultContentType(self):
|
||||
self.getPage('/')
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
self.getPage('/defct/plain')
|
||||
self.getPage('/')
|
||||
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
|
||||
self.getPage('/defct/html')
|
||||
|
||||
def test_multiple_headers(self):
|
||||
self.getPage('/multiheader/header_list')
|
||||
self.assertEqual(
|
||||
[(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
|
||||
[('WWW-Authenticate', 'Negotiate'),
|
||||
('WWW-Authenticate', 'Basic realm="foo"'),
|
||||
])
|
||||
self.getPage('/multiheader/commas')
|
||||
self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
|
||||
|
||||
def test_cherrypy_url(self):
|
||||
# Input relative to current
|
||||
self.getPage('/url/leaf?path_info=page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
self.getPage('/url/?path_info=page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
# Other host header
|
||||
host = 'www.mydomain.example'
|
||||
self.getPage('/url/leaf?path_info=page1',
|
||||
headers=[('Host', host)])
|
||||
self.assertBody('%s://%s/url/page1' % (self.scheme, host))
|
||||
|
||||
# Input is 'absolute'; that is, relative to script_name
|
||||
self.getPage('/url/leaf?path_info=/page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
self.getPage('/url/?path_info=/page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
|
||||
# Single dots
|
||||
self.getPage('/url/leaf?path_info=./page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
self.getPage('/url/leaf?path_info=other/./page1')
|
||||
self.assertBody('%s/url/other/page1' % self.base())
|
||||
self.getPage('/url/?path_info=/other/./page1')
|
||||
self.assertBody('%s/other/page1' % self.base())
|
||||
self.getPage('/url/?path_info=/other/././././page1')
|
||||
self.assertBody('%s/other/page1' % self.base())
|
||||
|
||||
# Double dots
|
||||
self.getPage('/url/leaf?path_info=../page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
self.getPage('/url/leaf?path_info=other/../page1')
|
||||
self.assertBody('%s/url/page1' % self.base())
|
||||
self.getPage('/url/leaf?path_info=/other/../page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
self.getPage('/url/leaf?path_info=/other/../../../page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
self.getPage('/url/leaf?path_info=/other/../../../../../page1')
|
||||
self.assertBody('%s/page1' % self.base())
|
||||
|
||||
# qs param is not normalized as a path
|
||||
self.getPage('/url/qs?qs=/other')
|
||||
self.assertBody('%s/url/qs?/other' % self.base())
|
||||
self.getPage('/url/qs?qs=/other/../page1')
|
||||
self.assertBody('%s/url/qs?/other/../page1' % self.base())
|
||||
self.getPage('/url/qs?qs=../page1')
|
||||
self.assertBody('%s/url/qs?../page1' % self.base())
|
||||
self.getPage('/url/qs?qs=../../page1')
|
||||
self.assertBody('%s/url/qs?../../page1' % self.base())
|
||||
|
||||
# Output relative to current path or script_name
|
||||
self.getPage('/url/?path_info=page1&relative=True')
|
||||
self.assertBody('page1')
|
||||
self.getPage('/url/leaf?path_info=/page1&relative=True')
|
||||
self.assertBody('../page1')
|
||||
self.getPage('/url/leaf?path_info=page1&relative=True')
|
||||
self.assertBody('page1')
|
||||
self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
|
||||
self.assertBody('leaf/page1')
|
||||
self.getPage('/url/leaf?path_info=../page1&relative=True')
|
||||
self.assertBody('../page1')
|
||||
self.getPage('/url/?path_info=other/../page1&relative=True')
|
||||
self.assertBody('page1')
|
||||
|
||||
# Output relative to /
|
||||
self.getPage('/baseurl?path_info=ab&relative=True')
|
||||
self.assertBody('ab')
|
||||
# Output relative to /
|
||||
self.getPage('/baseurl?path_info=/ab&relative=True')
|
||||
self.assertBody('ab')
|
||||
|
||||
# absolute-path references ("server-relative")
|
||||
# Input relative to current
|
||||
self.getPage('/url/leaf?path_info=page1&relative=server')
|
||||
self.assertBody('/url/page1')
|
||||
self.getPage('/url/?path_info=page1&relative=server')
|
||||
self.assertBody('/url/page1')
|
||||
# Input is 'absolute'; that is, relative to script_name
|
||||
self.getPage('/url/leaf?path_info=/page1&relative=server')
|
||||
self.assertBody('/page1')
|
||||
self.getPage('/url/?path_info=/page1&relative=server')
|
||||
self.assertBody('/page1')
|
||||
|
||||
def test_expose_decorator(self):
|
||||
# Test @expose
|
||||
self.getPage('/expose_dec/no_call')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr E. R. Bradshaw')
|
||||
|
||||
# Test @expose()
|
||||
self.getPage('/expose_dec/call_empty')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mrs. B.J. Smegma')
|
||||
|
||||
# Test @expose("alias")
|
||||
self.getPage('/expose_dec/call_alias')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr Nesbitt')
|
||||
# Does the original name work?
|
||||
self.getPage('/expose_dec/nesbitt')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr Nesbitt')
|
||||
|
||||
# Test @expose(["alias1", "alias2"])
|
||||
self.getPage('/expose_dec/alias1')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr Ken Andrews')
|
||||
self.getPage('/expose_dec/alias2')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr Ken Andrews')
|
||||
# Does the original name work?
|
||||
self.getPage('/expose_dec/andrews')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr Ken Andrews')
|
||||
|
||||
# Test @expose(alias="alias")
|
||||
self.getPage('/expose_dec/alias3')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Mr. and Mrs. Watson')
|
||||
|
||||
|
||||
class ErrorTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
def break_header():
|
||||
# Add a header after finalize that is invalid
|
||||
cherrypy.serving.response.header_list.append((2, 3))
|
||||
cherrypy.tools.break_header = cherrypy.Tool(
|
||||
'on_end_resource', break_header)
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'hello'
|
||||
|
||||
@cherrypy.config(**{'tools.break_header.on': True})
|
||||
def start_response_error(self):
|
||||
return 'salud!'
|
||||
|
||||
@cherrypy.expose
|
||||
def stat(self, path):
|
||||
with cherrypy.HTTPError.handle(OSError, 404):
|
||||
os.stat(path)
|
||||
|
||||
root = Root()
|
||||
|
||||
cherrypy.tree.mount(root)
|
||||
|
||||
def test_start_response_error(self):
|
||||
self.getPage('/start_response_error')
|
||||
self.assertStatus(500)
|
||||
self.assertInBody(
|
||||
'TypeError: response.header_list key 2 is not a byte string.')
|
||||
|
||||
def test_contextmanager(self):
|
||||
self.getPage('/stat/missing')
|
||||
self.assertStatus(404)
|
||||
body_text = self.body.decode('utf-8')
|
||||
assert (
|
||||
'No such file or directory' in body_text or
|
||||
'cannot find the file specified' in body_text
|
||||
)
|
||||
|
||||
|
||||
class TestBinding:
|
||||
def test_bind_ephemeral_port(self):
|
||||
"""
|
||||
A server configured to bind to port 0 will bind to an ephemeral
|
||||
port and indicate that port number on startup.
|
||||
"""
|
||||
cherrypy.config.reset()
|
||||
bind_ephemeral_conf = {
|
||||
'server.socket_port': 0,
|
||||
}
|
||||
cherrypy.config.update(bind_ephemeral_conf)
|
||||
cherrypy.engine.start()
|
||||
assert cherrypy.server.bound_addr != cherrypy.server.bind_addr
|
||||
_host, port = cherrypy.server.bound_addr
|
||||
assert port > 0
|
||||
cherrypy.engine.stop()
|
||||
assert cherrypy.server.bind_addr == cherrypy.server.bound_addr
|
||||
424
lib/cherrypy/test/test_dynamicobjectmapping.py
Normal file
424
lib/cherrypy/test/test_dynamicobjectmapping.py
Normal file
@@ -0,0 +1,424 @@
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
|
||||
|
||||
|
||||
def setup_server():
|
||||
class SubSubRoot:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'SubSubRoot index'
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
return 'SubSubRoot default'
|
||||
|
||||
@cherrypy.expose
|
||||
def handler(self):
|
||||
return 'SubSubRoot handler'
|
||||
|
||||
@cherrypy.expose
|
||||
def dispatch(self):
|
||||
return 'SubSubRoot dispatch'
|
||||
|
||||
subsubnodes = {
|
||||
'1': SubSubRoot(),
|
||||
'2': SubSubRoot(),
|
||||
}
|
||||
|
||||
class SubRoot:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'SubRoot index'
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
return 'SubRoot %s' % (args,)
|
||||
|
||||
@cherrypy.expose
|
||||
def handler(self):
|
||||
return 'SubRoot handler'
|
||||
|
||||
def _cp_dispatch(self, vpath):
|
||||
return subsubnodes.get(vpath[0], None)
|
||||
|
||||
subnodes = {
|
||||
'1': SubRoot(),
|
||||
'2': SubRoot(),
|
||||
}
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'index'
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
return 'default %s' % (args,)
|
||||
|
||||
@cherrypy.expose
|
||||
def handler(self):
|
||||
return 'handler'
|
||||
|
||||
def _cp_dispatch(self, vpath):
|
||||
return subnodes.get(vpath[0])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# DynamicNodeAndMethodDispatcher example.
|
||||
# This example exposes a fairly naive HTTP api
|
||||
class User(object):
|
||||
|
||||
def __init__(self, id, name):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
def __unicode__(self):
|
||||
return six.text_type(self.name)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
user_lookup = {
|
||||
1: User(1, 'foo'),
|
||||
2: User(2, 'bar'),
|
||||
}
|
||||
|
||||
def make_user(name, id=None):
|
||||
if not id:
|
||||
id = max(*list(user_lookup.keys())) + 1
|
||||
user_lookup[id] = User(id, name)
|
||||
return id
|
||||
|
||||
@cherrypy.expose
|
||||
class UserContainerNode(object):
|
||||
|
||||
def POST(self, name):
|
||||
"""
|
||||
Allow the creation of a new Object
|
||||
"""
|
||||
return 'POST %d' % make_user(name)
|
||||
|
||||
def GET(self):
|
||||
return six.text_type(sorted(user_lookup.keys()))
|
||||
|
||||
def dynamic_dispatch(self, vpath):
|
||||
try:
|
||||
id = int(vpath[0])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
return UserInstanceNode(id)
|
||||
|
||||
@cherrypy.expose
|
||||
class UserInstanceNode(object):
|
||||
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.user = user_lookup.get(id, None)
|
||||
|
||||
# For all but PUT methods there MUST be a valid user identified
|
||||
# by self.id
|
||||
if not self.user and cherrypy.request.method != 'PUT':
|
||||
raise cherrypy.HTTPError(404)
|
||||
|
||||
def GET(self, *args, **kwargs):
|
||||
"""
|
||||
Return the appropriate representation of the instance.
|
||||
"""
|
||||
return six.text_type(self.user)
|
||||
|
||||
def POST(self, name):
|
||||
"""
|
||||
Update the fields of the user instance.
|
||||
"""
|
||||
self.user.name = name
|
||||
return 'POST %d' % self.user.id
|
||||
|
||||
def PUT(self, name):
|
||||
"""
|
||||
Create a new user with the specified id, or edit it if it already
|
||||
exists
|
||||
"""
|
||||
if self.user:
|
||||
# Edit the current user
|
||||
self.user.name = name
|
||||
return 'PUT %d' % self.user.id
|
||||
else:
|
||||
# Make a new user with said attributes.
|
||||
return 'PUT %d' % make_user(name, self.id)
|
||||
|
||||
def DELETE(self):
|
||||
"""
|
||||
Delete the user specified at the id.
|
||||
"""
|
||||
id = self.user.id
|
||||
del user_lookup[self.user.id]
|
||||
del self.user
|
||||
return 'DELETE %d' % id
|
||||
|
||||
class ABHandler:
|
||||
|
||||
class CustomDispatch:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, a, b):
|
||||
return 'custom'
|
||||
|
||||
def _cp_dispatch(self, vpath):
|
||||
"""Make sure that if we don't pop anything from vpath,
|
||||
processing still works.
|
||||
"""
|
||||
return self.CustomDispatch()
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, a, b=None):
|
||||
body = ['a:' + str(a)]
|
||||
if b is not None:
|
||||
body.append(',b:' + str(b))
|
||||
return ''.join(body)
|
||||
|
||||
@cherrypy.expose
|
||||
def delete(self, a, b):
|
||||
return 'deleting ' + str(a) + ' and ' + str(b)
|
||||
|
||||
class IndexOnly:
|
||||
|
||||
def _cp_dispatch(self, vpath):
|
||||
"""Make sure that popping ALL of vpath still shows the index
|
||||
handler.
|
||||
"""
|
||||
while vpath:
|
||||
vpath.pop()
|
||||
return self
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'IndexOnly index'
|
||||
|
||||
class DecoratedPopArgs:
|
||||
|
||||
"""Test _cp_dispatch with @cherrypy.popargs."""
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'no params'
|
||||
|
||||
@cherrypy.expose
|
||||
def hi(self):
|
||||
return "hi was not interpreted as 'a' param"
|
||||
DecoratedPopArgs = cherrypy.popargs(
|
||||
'a', 'b', handler=ABHandler())(DecoratedPopArgs)
|
||||
|
||||
class NonDecoratedPopArgs:
|
||||
|
||||
"""Test _cp_dispatch = cherrypy.popargs()"""
|
||||
|
||||
_cp_dispatch = cherrypy.popargs('a')
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, a):
|
||||
return 'index: ' + str(a)
|
||||
|
||||
class ParameterizedHandler:
|
||||
|
||||
"""Special handler created for each request"""
|
||||
|
||||
def __init__(self, a):
|
||||
self.a = a
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
if 'a' in cherrypy.request.params:
|
||||
raise Exception(
|
||||
'Parameterized handler argument ended up in '
|
||||
'request.params')
|
||||
return self.a
|
||||
|
||||
class ParameterizedPopArgs:
|
||||
|
||||
"""Test cherrypy.popargs() with a function call handler"""
|
||||
ParameterizedPopArgs = cherrypy.popargs(
|
||||
'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
|
||||
|
||||
Root.decorated = DecoratedPopArgs()
|
||||
Root.undecorated = NonDecoratedPopArgs()
|
||||
Root.index_only = IndexOnly()
|
||||
Root.parameter_test = ParameterizedPopArgs()
|
||||
|
||||
Root.users = UserContainerNode()
|
||||
|
||||
md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
|
||||
for url in script_names:
|
||||
conf = {
|
||||
'/': {
|
||||
'user': (url or '/').split('/')[-2],
|
||||
},
|
||||
'/users': {
|
||||
'request.dispatch': md
|
||||
},
|
||||
}
|
||||
cherrypy.tree.mount(Root(), url, conf)
|
||||
|
||||
|
||||
class DynamicObjectMappingTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testObjectMapping(self):
|
||||
for url in script_names:
|
||||
self.script_name = url
|
||||
|
||||
self.getPage('/')
|
||||
self.assertBody('index')
|
||||
|
||||
self.getPage('/handler')
|
||||
self.assertBody('handler')
|
||||
|
||||
# Dynamic dispatch will succeed here for the subnodes
|
||||
# so the subroot gets called
|
||||
self.getPage('/1/')
|
||||
self.assertBody('SubRoot index')
|
||||
|
||||
self.getPage('/2/')
|
||||
self.assertBody('SubRoot index')
|
||||
|
||||
self.getPage('/1/handler')
|
||||
self.assertBody('SubRoot handler')
|
||||
|
||||
self.getPage('/2/handler')
|
||||
self.assertBody('SubRoot handler')
|
||||
|
||||
# Dynamic dispatch will fail here for the subnodes
|
||||
# so the default gets called
|
||||
self.getPage('/asdf/')
|
||||
self.assertBody("default ('asdf',)")
|
||||
|
||||
self.getPage('/asdf/asdf')
|
||||
self.assertBody("default ('asdf', 'asdf')")
|
||||
|
||||
self.getPage('/asdf/handler')
|
||||
self.assertBody("default ('asdf', 'handler')")
|
||||
|
||||
# Dynamic dispatch will succeed here for the subsubnodes
|
||||
# so the subsubroot gets called
|
||||
self.getPage('/1/1/')
|
||||
self.assertBody('SubSubRoot index')
|
||||
|
||||
self.getPage('/2/2/')
|
||||
self.assertBody('SubSubRoot index')
|
||||
|
||||
self.getPage('/1/1/handler')
|
||||
self.assertBody('SubSubRoot handler')
|
||||
|
||||
self.getPage('/2/2/handler')
|
||||
self.assertBody('SubSubRoot handler')
|
||||
|
||||
self.getPage('/2/2/dispatch')
|
||||
self.assertBody('SubSubRoot dispatch')
|
||||
|
||||
# The exposed dispatch will not be called as a dispatch
|
||||
# method.
|
||||
self.getPage('/2/2/foo/foo')
|
||||
self.assertBody('SubSubRoot default')
|
||||
|
||||
# Dynamic dispatch will fail here for the subsubnodes
|
||||
# so the SubRoot gets called
|
||||
self.getPage('/1/asdf/')
|
||||
self.assertBody("SubRoot ('asdf',)")
|
||||
|
||||
self.getPage('/1/asdf/asdf')
|
||||
self.assertBody("SubRoot ('asdf', 'asdf')")
|
||||
|
||||
self.getPage('/1/asdf/handler')
|
||||
self.assertBody("SubRoot ('asdf', 'handler')")
|
||||
|
||||
def testMethodDispatch(self):
|
||||
# GET acts like a container
|
||||
self.getPage('/users')
|
||||
self.assertBody('[1, 2]')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
# POST to the container URI allows creation
|
||||
self.getPage('/users', method='POST', body='name=baz')
|
||||
self.assertBody('POST 3')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
# POST to a specific instanct URI results in a 404
|
||||
# as the resource does not exit.
|
||||
self.getPage('/users/5', method='POST', body='name=baz')
|
||||
self.assertStatus(404)
|
||||
|
||||
# PUT to a specific instanct URI results in creation
|
||||
self.getPage('/users/5', method='PUT', body='name=boris')
|
||||
self.assertBody('PUT 5')
|
||||
self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
|
||||
|
||||
# GET acts like a container
|
||||
self.getPage('/users')
|
||||
self.assertBody('[1, 2, 3, 5]')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
test_cases = (
|
||||
(1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
|
||||
(2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
|
||||
(3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
|
||||
(5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
|
||||
)
|
||||
for id, name, updatedname, headers in test_cases:
|
||||
self.getPage('/users/%d' % id)
|
||||
self.assertBody(name)
|
||||
self.assertHeader('Allow', headers)
|
||||
|
||||
# Make sure POSTs update already existings resources
|
||||
self.getPage('/users/%d' %
|
||||
id, method='POST', body='name=%s' % updatedname)
|
||||
self.assertBody('POST %d' % id)
|
||||
self.assertHeader('Allow', headers)
|
||||
|
||||
# Make sure PUTs Update already existing resources.
|
||||
self.getPage('/users/%d' %
|
||||
id, method='PUT', body='name=%s' % updatedname)
|
||||
self.assertBody('PUT %d' % id)
|
||||
self.assertHeader('Allow', headers)
|
||||
|
||||
# Make sure DELETES Remove already existing resources.
|
||||
self.getPage('/users/%d' % id, method='DELETE')
|
||||
self.assertBody('DELETE %d' % id)
|
||||
self.assertHeader('Allow', headers)
|
||||
|
||||
# GET acts like a container
|
||||
self.getPage('/users')
|
||||
self.assertBody('[]')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
def testVpathDispatch(self):
|
||||
self.getPage('/decorated/')
|
||||
self.assertBody('no params')
|
||||
|
||||
self.getPage('/decorated/hi')
|
||||
self.assertBody("hi was not interpreted as 'a' param")
|
||||
|
||||
self.getPage('/decorated/yo/')
|
||||
self.assertBody('a:yo')
|
||||
|
||||
self.getPage('/decorated/yo/there/')
|
||||
self.assertBody('a:yo,b:there')
|
||||
|
||||
self.getPage('/decorated/yo/there/delete')
|
||||
self.assertBody('deleting yo and there')
|
||||
|
||||
self.getPage('/decorated/yo/there/handled_by_dispatch/')
|
||||
self.assertBody('custom')
|
||||
|
||||
self.getPage('/undecorated/blah/')
|
||||
self.assertBody('index: blah')
|
||||
|
||||
self.getPage('/index_only/a/b/c/d/e/f/g/')
|
||||
self.assertBody('IndexOnly index')
|
||||
|
||||
self.getPage('/parameter_test/argument2/')
|
||||
self.assertBody('argument2')
|
||||
433
lib/cherrypy/test/test_encoding.py
Normal file
433
lib/cherrypy/test/test_encoding.py
Normal file
@@ -0,0 +1,433 @@
|
||||
# coding: utf-8
|
||||
|
||||
import gzip
|
||||
import io
|
||||
from unittest import mock
|
||||
|
||||
from six.moves.http_client import IncompleteRead
|
||||
from six.moves.urllib.parse import quote as url_quote
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, ntou
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
europoundUnicode = ntou('£', encoding='utf-8')
|
||||
sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8')
|
||||
|
||||
sing8 = sing.encode('utf-8')
|
||||
sing16 = sing.encode('utf-16')
|
||||
|
||||
|
||||
class EncodingTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, param):
|
||||
assert param == europoundUnicode, '%r != %r' % (
|
||||
param, europoundUnicode)
|
||||
yield europoundUnicode
|
||||
|
||||
@cherrypy.expose
|
||||
def mao_zedong(self):
|
||||
return sing
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.encode.encoding': 'utf-8'})
|
||||
def utf8(self):
|
||||
return sing8
|
||||
|
||||
@cherrypy.expose
|
||||
def cookies_and_headers(self):
|
||||
# if the headers have non-ascii characters and a cookie has
|
||||
# any part which is unicode (even ascii), the response
|
||||
# should not fail.
|
||||
cherrypy.response.cookie['candy'] = 'bar'
|
||||
cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
|
||||
cherrypy.response.headers[
|
||||
'Some-Header'] = 'My d\xc3\xb6g has fleas'
|
||||
cherrypy.response.headers[
|
||||
'Bytes-Header'] = b'Bytes given header'
|
||||
return 'Any content'
|
||||
|
||||
@cherrypy.expose
|
||||
def reqparams(self, *args, **kwargs):
|
||||
return b', '.join(
|
||||
[': '.join((k, v)).encode('utf8')
|
||||
for k, v in sorted(cherrypy.request.params.items())]
|
||||
)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.encode.text_only': False,
|
||||
'tools.encode.add_charset': True,
|
||||
})
|
||||
def nontext(self, *args, **kwargs):
|
||||
cherrypy.response.headers[
|
||||
'Content-Type'] = 'application/binary'
|
||||
return '\x00\x01\x02\x03'
|
||||
|
||||
class GZIP:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
yield 'Hello, world'
|
||||
|
||||
@cherrypy.expose
|
||||
# Turn encoding off so the gzip tool is the one doing the collapse.
|
||||
@cherrypy.config(**{'tools.encode.on': False})
|
||||
def noshow(self):
|
||||
# Test for ticket #147, where yield showed no exceptions
|
||||
# (content-encoding was still gzip even though traceback
|
||||
# wasn't zipped).
|
||||
raise IndexError()
|
||||
yield 'Here be dragons'
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
def noshow_stream(self):
|
||||
# Test for ticket #147, where yield showed no exceptions
|
||||
# (content-encoding was still gzip even though traceback
|
||||
# wasn't zipped).
|
||||
raise IndexError()
|
||||
yield 'Here be dragons'
|
||||
|
||||
class Decode:
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.decode.on': True,
|
||||
'tools.decode.default_encoding': ['utf-16'],
|
||||
})
|
||||
def extra_charset(self, *args, **kwargs):
|
||||
return ', '.join([': '.join((k, v))
|
||||
for k, v in cherrypy.request.params.items()])
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.decode.on': True,
|
||||
'tools.decode.encoding': 'utf-16',
|
||||
})
|
||||
def force_charset(self, *args, **kwargs):
|
||||
return ', '.join([': '.join((k, v))
|
||||
for k, v in cherrypy.request.params.items()])
|
||||
|
||||
root = Root()
|
||||
root.gzip = GZIP()
|
||||
root.decode = Decode()
|
||||
cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
|
||||
|
||||
def test_query_string_decoding(self):
|
||||
URI_TMPL = '/reqparams?q={q}'
|
||||
|
||||
europoundUtf8_2_bytes = europoundUnicode.encode('utf-8')
|
||||
europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2]
|
||||
|
||||
# Encoded utf8 query strings MUST be parsed correctly.
|
||||
# Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
|
||||
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes)))
|
||||
# The return value will be encoded as utf8.
|
||||
self.assertBody(b'q: ' + europoundUtf8_2_bytes)
|
||||
|
||||
# Query strings that are incorrectly encoded MUST raise 404.
|
||||
# Here, q is the second byte of POUND SIGN U+A3 encoded in utf8
|
||||
# and then %HEX
|
||||
# TODO: check whether this shouldn't raise 400 Bad Request instead
|
||||
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte)))
|
||||
self.assertStatus(404)
|
||||
self.assertErrorPage(
|
||||
404,
|
||||
'The given query string could not be processed. Query '
|
||||
"strings for this resource must be encoded with 'utf8'.")
|
||||
|
||||
def test_urlencoded_decoding(self):
|
||||
# Test the decoding of an application/x-www-form-urlencoded entity.
|
||||
europoundUtf8 = europoundUnicode.encode('utf-8')
|
||||
body = b'param=' + europoundUtf8
|
||||
self.getPage('/',
|
||||
method='POST',
|
||||
headers=[
|
||||
('Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(europoundUtf8)
|
||||
|
||||
# Encoded utf8 entities MUST be parsed and decoded correctly.
|
||||
# Here, q is the POUND SIGN U+00A3 encoded in utf8
|
||||
body = b'q=\xc2\xa3'
|
||||
self.getPage('/reqparams', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'q: \xc2\xa3')
|
||||
|
||||
# ...and in utf16, which is not in the default attempt_charsets list:
|
||||
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
|
||||
self.getPage('/reqparams',
|
||||
method='POST',
|
||||
headers=[
|
||||
('Content-Type',
|
||||
'application/x-www-form-urlencoded;charset=utf-16'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'q: \xc2\xa3')
|
||||
|
||||
# Entities that are incorrectly encoded MUST raise 400.
|
||||
# Here, q is the POUND SIGN U+00A3 encoded in utf16, but
|
||||
# the Content-Type incorrectly labels it utf-8.
|
||||
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
|
||||
self.getPage('/reqparams',
|
||||
method='POST',
|
||||
headers=[
|
||||
('Content-Type',
|
||||
'application/x-www-form-urlencoded;charset=utf-8'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertStatus(400)
|
||||
self.assertErrorPage(
|
||||
400,
|
||||
'The request entity could not be decoded. The following charsets '
|
||||
"were attempted: ['utf-8']")
|
||||
|
||||
def test_decode_tool(self):
|
||||
# An extra charset should be tried first, and succeed if it matches.
|
||||
# Here, we add utf-16 as a charset and pass a utf-16 body.
|
||||
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
|
||||
self.getPage('/decode/extra_charset', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'q: \xc2\xa3')
|
||||
|
||||
# An extra charset should be tried first, and continue to other default
|
||||
# charsets if it doesn't match.
|
||||
# Here, we add utf-16 as a charset but still pass a utf-8 body.
|
||||
body = b'q=\xc2\xa3'
|
||||
self.getPage('/decode/extra_charset', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'q: \xc2\xa3')
|
||||
|
||||
# An extra charset should error if force is True and it doesn't match.
|
||||
# Here, we force utf-16 as a charset but still pass a utf-8 body.
|
||||
body = b'q=\xc2\xa3'
|
||||
self.getPage('/decode/force_charset', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'application/x-www-form-urlencoded'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertErrorPage(
|
||||
400,
|
||||
'The request entity could not be decoded. The following charsets '
|
||||
"were attempted: ['utf-16']")
|
||||
|
||||
def test_multipart_decoding(self):
|
||||
# Test the decoding of a multipart entity when the charset (utf16) is
|
||||
# explicitly given.
|
||||
body = ntob('\r\n'.join([
|
||||
'--X',
|
||||
'Content-Type: text/plain;charset=utf-16',
|
||||
'Content-Disposition: form-data; name="text"',
|
||||
'',
|
||||
'\xff\xfea\x00b\x00\x1c c\x00',
|
||||
'--X',
|
||||
'Content-Type: text/plain;charset=utf-16',
|
||||
'Content-Disposition: form-data; name="submit"',
|
||||
'',
|
||||
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
|
||||
'--X--'
|
||||
]))
|
||||
self.getPage('/reqparams', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'multipart/form-data;boundary=X'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc')
|
||||
|
||||
@mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1)
|
||||
def test_multipart_decoding_bigger_maxrambytes(self):
|
||||
"""
|
||||
Decoding of a multipart entity should also pass when
|
||||
the entity is bigger than maxrambytes. See ticket #1352.
|
||||
"""
|
||||
self.test_multipart_decoding()
|
||||
|
||||
def test_multipart_decoding_no_charset(self):
|
||||
# Test the decoding of a multipart entity when the charset (utf8) is
|
||||
# NOT explicitly given, but is in the list of charsets to attempt.
|
||||
body = ntob('\r\n'.join([
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="text"',
|
||||
'',
|
||||
'\xe2\x80\x9c',
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="submit"',
|
||||
'',
|
||||
'Create',
|
||||
'--X--'
|
||||
]))
|
||||
self.getPage('/reqparams', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'multipart/form-data;boundary=X'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(b'submit: Create, text: \xe2\x80\x9c')
|
||||
|
||||
def test_multipart_decoding_no_successful_charset(self):
|
||||
# Test the decoding of a multipart entity when the charset (utf16) is
|
||||
# NOT explicitly given, and is NOT in the list of charsets to attempt.
|
||||
body = ntob('\r\n'.join([
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="text"',
|
||||
'',
|
||||
'\xff\xfea\x00b\x00\x1c c\x00',
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="submit"',
|
||||
'',
|
||||
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
|
||||
'--X--'
|
||||
]))
|
||||
self.getPage('/reqparams', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'multipart/form-data;boundary=X'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertStatus(400)
|
||||
self.assertErrorPage(
|
||||
400,
|
||||
'The request entity could not be decoded. The following charsets '
|
||||
"were attempted: ['us-ascii', 'utf-8']")
|
||||
|
||||
def test_nontext(self):
|
||||
self.getPage('/nontext')
|
||||
self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
|
||||
self.assertBody('\x00\x01\x02\x03')
|
||||
|
||||
def testEncoding(self):
|
||||
# Default encoding should be utf-8
|
||||
self.getPage('/mao_zedong')
|
||||
self.assertBody(sing8)
|
||||
|
||||
# Ask for utf-16.
|
||||
self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-16')
|
||||
self.assertBody(sing16)
|
||||
|
||||
# Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
|
||||
# should be produced.
|
||||
self.getPage('/mao_zedong', [('Accept-Charset',
|
||||
'iso-8859-1;q=1, utf-16;q=0.5')])
|
||||
self.assertBody(sing16)
|
||||
|
||||
# The "*" value should default to our default_encoding, utf-8
|
||||
self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
|
||||
self.assertBody(sing8)
|
||||
|
||||
# Only allow iso-8859-1, which should fail and raise 406.
|
||||
self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
|
||||
self.assertStatus('406 Not Acceptable')
|
||||
self.assertInBody('Your client sent this Accept-Charset header: '
|
||||
'iso-8859-1, *;q=0. We tried these charsets: '
|
||||
'iso-8859-1.')
|
||||
|
||||
# Ask for x-mac-ce, which should be unknown. See ticket #569.
|
||||
self.getPage('/mao_zedong', [('Accept-Charset',
|
||||
'us-ascii, ISO-8859-1, x-mac-ce')])
|
||||
self.assertStatus('406 Not Acceptable')
|
||||
self.assertInBody('Your client sent this Accept-Charset header: '
|
||||
'us-ascii, ISO-8859-1, x-mac-ce. We tried these '
|
||||
'charsets: ISO-8859-1, us-ascii, x-mac-ce.')
|
||||
|
||||
# Test the 'encoding' arg to encode.
|
||||
self.getPage('/utf8')
|
||||
self.assertBody(sing8)
|
||||
self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
|
||||
self.assertStatus('406 Not Acceptable')
|
||||
|
||||
# Test malformed quality value, which should raise 400.
|
||||
self.getPage('/mao_zedong', [('Accept-Charset',
|
||||
'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')])
|
||||
self.assertStatus('400 Bad Request')
|
||||
|
||||
def testGzip(self):
|
||||
zbuf = io.BytesIO()
|
||||
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
|
||||
zfile.write(b'Hello, world')
|
||||
zfile.close()
|
||||
|
||||
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertInBody(zbuf.getvalue()[:3])
|
||||
self.assertHeader('Vary', 'Accept-Encoding')
|
||||
self.assertHeader('Content-Encoding', 'gzip')
|
||||
|
||||
# Test when gzip is denied.
|
||||
self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')])
|
||||
self.assertHeader('Vary', 'Accept-Encoding')
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
self.assertBody('Hello, world')
|
||||
|
||||
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')])
|
||||
self.assertHeader('Vary', 'Accept-Encoding')
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
self.assertBody('Hello, world')
|
||||
|
||||
# Test that trailing comma doesn't cause IndexError
|
||||
# Ref: https://github.com/cherrypy/cherrypy/issues/988
|
||||
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')])
|
||||
self.assertStatus(200)
|
||||
self.assertNotInBody('IndexError')
|
||||
|
||||
self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')])
|
||||
self.assertStatus(406)
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
self.assertErrorPage(406, 'identity, gzip')
|
||||
|
||||
# Test for ticket #147
|
||||
self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertNoHeader('Content-Encoding')
|
||||
self.assertStatus(500)
|
||||
self.assertErrorPage(500, pattern='IndexError\n')
|
||||
|
||||
# In this case, there's nothing we can do to deliver a
|
||||
# readable page, since 1) the gzip header is already set,
|
||||
# and 2) we may have already written some of the body.
|
||||
# The fix is to never stream yields when using gzip.
|
||||
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
|
||||
getattr(cherrypy.server, 'using_apache', False)):
|
||||
self.getPage('/gzip/noshow_stream',
|
||||
headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertHeader('Content-Encoding', 'gzip')
|
||||
self.assertInBody('\x1f\x8b\x08\x00')
|
||||
else:
|
||||
# The wsgiserver will simply stop sending data, and the HTTP client
|
||||
# will error due to an incomplete chunk-encoded stream.
|
||||
self.assertRaises((ValueError, IncompleteRead), self.getPage,
|
||||
'/gzip/noshow_stream',
|
||||
headers=[('Accept-Encoding', 'gzip')])
|
||||
|
||||
def test_UnicodeHeaders(self):
|
||||
self.getPage('/cookies_and_headers')
|
||||
self.assertBody('Any content')
|
||||
|
||||
def test_BytesHeaders(self):
|
||||
self.getPage('/cookies_and_headers')
|
||||
self.assertBody('Any content')
|
||||
self.assertHeader('Bytes-Header', 'Bytes given header')
|
||||
84
lib/cherrypy/test/test_etags.py
Normal file
84
lib/cherrypy/test/test_etags.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class ETagTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def resource(self):
|
||||
return 'Oh wah ta goo Siam.'
|
||||
|
||||
@cherrypy.expose
|
||||
def fail(self, code):
|
||||
code = int(code)
|
||||
if 300 <= code <= 399:
|
||||
raise cherrypy.HTTPRedirect([], code)
|
||||
else:
|
||||
raise cherrypy.HTTPError(code)
|
||||
|
||||
@cherrypy.expose
|
||||
# In Python 3, tools.encode is on by default
|
||||
@cherrypy.config(**{'tools.encode.on': True})
|
||||
def unicoded(self):
|
||||
return ntou('I am a \u1ee4nicode string.', 'escape')
|
||||
|
||||
conf = {'/': {'tools.etags.on': True,
|
||||
'tools.etags.autotags': True,
|
||||
}}
|
||||
cherrypy.tree.mount(Root(), config=conf)
|
||||
|
||||
def test_etags(self):
|
||||
self.getPage('/resource')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
self.assertBody('Oh wah ta goo Siam.')
|
||||
etag = self.assertHeader('ETag')
|
||||
|
||||
# Test If-Match (both valid and invalid)
|
||||
self.getPage('/resource', headers=[('If-Match', etag)])
|
||||
self.assertStatus('200 OK')
|
||||
self.getPage('/resource', headers=[('If-Match', '*')])
|
||||
self.assertStatus('200 OK')
|
||||
self.getPage('/resource', headers=[('If-Match', '*')], method='POST')
|
||||
self.assertStatus('200 OK')
|
||||
self.getPage('/resource', headers=[('If-Match', 'a bogus tag')])
|
||||
self.assertStatus('412 Precondition Failed')
|
||||
|
||||
# Test If-None-Match (both valid and invalid)
|
||||
self.getPage('/resource', headers=[('If-None-Match', etag)])
|
||||
self.assertStatus(304)
|
||||
self.getPage('/resource', method='POST',
|
||||
headers=[('If-None-Match', etag)])
|
||||
self.assertStatus('412 Precondition Failed')
|
||||
self.getPage('/resource', headers=[('If-None-Match', '*')])
|
||||
self.assertStatus(304)
|
||||
self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')])
|
||||
self.assertStatus('200 OK')
|
||||
|
||||
def test_errors(self):
|
||||
self.getPage('/resource')
|
||||
self.assertStatus(200)
|
||||
etag = self.assertHeader('ETag')
|
||||
|
||||
# Test raising errors in page handler
|
||||
self.getPage('/fail/412', headers=[('If-Match', etag)])
|
||||
self.assertStatus(412)
|
||||
self.getPage('/fail/304', headers=[('If-Match', etag)])
|
||||
self.assertStatus(304)
|
||||
self.getPage('/fail/412', headers=[('If-None-Match', '*')])
|
||||
self.assertStatus(412)
|
||||
self.getPage('/fail/304', headers=[('If-None-Match', '*')])
|
||||
self.assertStatus(304)
|
||||
|
||||
def test_unicode_body(self):
|
||||
self.getPage('/unicoded')
|
||||
self.assertStatus(200)
|
||||
etag1 = self.assertHeader('ETag')
|
||||
self.getPage('/unicoded', headers=[('If-Match', etag1)])
|
||||
self.assertStatus(200)
|
||||
self.assertHeader('ETag', etag1)
|
||||
307
lib/cherrypy/test/test_http.py
Normal file
307
lib/cherrypy/test/test_http.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# coding: utf-8
|
||||
"""Tests for managing HTTP issues (malformed requests, etc)."""
|
||||
|
||||
import errno
|
||||
import mimetypes
|
||||
import socket
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import six
|
||||
from six.moves.http_client import HTTPConnection
|
||||
from six.moves import urllib
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import HTTPSConnection, quote
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
def is_ascii(text):
|
||||
"""
|
||||
Return True if the text encodes as ascii.
|
||||
"""
|
||||
try:
|
||||
text.encode('ascii')
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def encode_filename(filename):
|
||||
"""
|
||||
Given a filename to be used in a multipart/form-data,
|
||||
encode the name. Return the key and encoded filename.
|
||||
"""
|
||||
if is_ascii(filename):
|
||||
return 'filename', '"{filename}"'.format(**locals())
|
||||
encoded = quote(filename, encoding='utf-8')
|
||||
return 'filename*', "'".join((
|
||||
'UTF-8',
|
||||
'', # lang
|
||||
encoded,
|
||||
))
|
||||
|
||||
|
||||
def encode_multipart_formdata(files):
|
||||
"""Return (content_type, body) ready for httplib.HTTP instance.
|
||||
|
||||
files: a sequence of (name, filename, value) tuples for multipart uploads.
|
||||
filename can be a string or a tuple ('filename string', 'encoding')
|
||||
"""
|
||||
BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
|
||||
L = []
|
||||
for key, filename, value in files:
|
||||
L.append('--' + BOUNDARY)
|
||||
|
||||
fn_key, encoded = encode_filename(filename)
|
||||
tmpl = \
|
||||
'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}'
|
||||
L.append(tmpl.format(**locals()))
|
||||
ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
L.append('Content-Type: %s' % ct)
|
||||
L.append('')
|
||||
L.append(value)
|
||||
L.append('--' + BOUNDARY + '--')
|
||||
L.append('')
|
||||
body = '\r\n'.join(L)
|
||||
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
return content_type, body
|
||||
|
||||
|
||||
class HTTPTests(helper.CPWebCase):
|
||||
|
||||
def make_connection(self):
|
||||
if self.scheme == 'https':
|
||||
return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
else:
|
||||
return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, *args, **kwargs):
|
||||
return 'Hello world!'
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'request.process_request_body': False})
|
||||
def no_body(self, *args, **kwargs):
|
||||
return 'Hello world!'
|
||||
|
||||
@cherrypy.expose
|
||||
def post_multipart(self, file):
|
||||
"""Return a summary ("a * 65536\nb * 65536") of the uploaded
|
||||
file.
|
||||
"""
|
||||
contents = file.file.read()
|
||||
summary = []
|
||||
curchar = None
|
||||
count = 0
|
||||
for c in contents:
|
||||
if c == curchar:
|
||||
count += 1
|
||||
else:
|
||||
if count:
|
||||
if six.PY3:
|
||||
curchar = chr(curchar)
|
||||
summary.append('%s * %d' % (curchar, count))
|
||||
count = 1
|
||||
curchar = c
|
||||
if count:
|
||||
if six.PY3:
|
||||
curchar = chr(curchar)
|
||||
summary.append('%s * %d' % (curchar, count))
|
||||
return ', '.join(summary)
|
||||
|
||||
@cherrypy.expose
|
||||
def post_filename(self, myfile):
|
||||
'''Return the name of the file which was uploaded.'''
|
||||
return myfile.filename
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({'server.max_request_body_size': 30000000})
|
||||
|
||||
def test_no_content_length(self):
|
||||
# "The presence of a message-body in a request is signaled by the
|
||||
# inclusion of a Content-Length or Transfer-Encoding header field in
|
||||
# the request's message-headers."
|
||||
#
|
||||
# Send a message with neither header and no body. Even though
|
||||
# the request is of method POST, this should be OK because we set
|
||||
# request.process_request_body to False for our handler.
|
||||
c = self.make_connection()
|
||||
c.request('POST', '/no_body')
|
||||
response = c.getresponse()
|
||||
self.body = response.fp.read()
|
||||
self.status = str(response.status)
|
||||
self.assertStatus(200)
|
||||
self.assertBody(b'Hello world!')
|
||||
|
||||
# Now send a message that has no Content-Length, but does send a body.
|
||||
# Verify that CP times out the socket and responds
|
||||
# with 411 Length Required.
|
||||
if self.scheme == 'https':
|
||||
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
else:
|
||||
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
|
||||
# `_get_content_length` is needed for Python 3.6+
|
||||
with mock.patch.object(
|
||||
c,
|
||||
'_get_content_length',
|
||||
lambda body, method: None,
|
||||
create=True):
|
||||
# `_set_content_length` is needed for Python 2.7-3.5
|
||||
with mock.patch.object(c, '_set_content_length', create=True):
|
||||
c.request('POST', '/')
|
||||
|
||||
response = c.getresponse()
|
||||
self.body = response.fp.read()
|
||||
self.status = str(response.status)
|
||||
self.assertStatus(411)
|
||||
|
||||
def test_post_multipart(self):
|
||||
alphabet = 'abcdefghijklmnopqrstuvwxyz'
|
||||
# generate file contents for a large post
|
||||
contents = ''.join([c * 65536 for c in alphabet])
|
||||
|
||||
# encode as multipart form data
|
||||
files = [('file', 'file.txt', contents)]
|
||||
content_type, body = encode_multipart_formdata(files)
|
||||
body = body.encode('Latin-1')
|
||||
|
||||
# post file
|
||||
c = self.make_connection()
|
||||
c.putrequest('POST', '/post_multipart')
|
||||
c.putheader('Content-Type', content_type)
|
||||
c.putheader('Content-Length', str(len(body)))
|
||||
c.endheaders()
|
||||
c.send(body)
|
||||
|
||||
response = c.getresponse()
|
||||
self.body = response.fp.read()
|
||||
self.status = str(response.status)
|
||||
self.assertStatus(200)
|
||||
parts = ['%s * 65536' % ch for ch in alphabet]
|
||||
self.assertBody(', '.join(parts))
|
||||
|
||||
def test_post_filename_with_special_characters(self):
|
||||
'''Testing that we can handle filenames with special characters. This
|
||||
was reported as a bug in:
|
||||
https://github.com/cherrypy/cherrypy/issues/1146/
|
||||
https://github.com/cherrypy/cherrypy/issues/1397/
|
||||
https://github.com/cherrypy/cherrypy/issues/1694/
|
||||
'''
|
||||
# We'll upload a bunch of files with differing names.
|
||||
fnames = [
|
||||
'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
|
||||
'file;name.csv', 'file; name.csv', u'test_łóąä.txt',
|
||||
]
|
||||
for fname in fnames:
|
||||
files = [('myfile', fname, 'yunyeenyunyue')]
|
||||
content_type, body = encode_multipart_formdata(files)
|
||||
body = body.encode('Latin-1')
|
||||
|
||||
# post file
|
||||
c = self.make_connection()
|
||||
c.putrequest('POST', '/post_filename')
|
||||
c.putheader('Content-Type', content_type)
|
||||
c.putheader('Content-Length', str(len(body)))
|
||||
c.endheaders()
|
||||
c.send(body)
|
||||
|
||||
response = c.getresponse()
|
||||
self.body = response.fp.read()
|
||||
self.status = str(response.status)
|
||||
self.assertStatus(200)
|
||||
self.assertBody(fname)
|
||||
|
||||
def test_malformed_request_line(self):
|
||||
if getattr(cherrypy.server, 'using_apache', False):
|
||||
return self.skip('skipped due to known Apache differences...')
|
||||
|
||||
# Test missing version in Request-Line
|
||||
c = self.make_connection()
|
||||
c._output(b'geT /')
|
||||
c._send_output()
|
||||
if hasattr(c, 'strict'):
|
||||
response = c.response_class(c.sock, strict=c.strict, method='GET')
|
||||
else:
|
||||
# Python 3.2 removed the 'strict' feature, saying:
|
||||
# "http.client now always assumes HTTP/1.x compliant servers."
|
||||
response = c.response_class(c.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 400)
|
||||
self.assertEqual(response.fp.read(22), b'Malformed Request-Line')
|
||||
c.close()
|
||||
|
||||
def test_request_line_split_issue_1220(self):
|
||||
params = {
|
||||
'intervenant-entreprise-evenement_classaction':
|
||||
'evenement-mailremerciements',
|
||||
'_path': 'intervenant-entreprise-evenement',
|
||||
'intervenant-entreprise-evenement_action-id': 19404,
|
||||
'intervenant-entreprise-evenement_id': 19404,
|
||||
'intervenant-entreprise_id': 28092,
|
||||
}
|
||||
Request_URI = '/index?' + urllib.parse.urlencode(params)
|
||||
self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256)
|
||||
self.getPage(Request_URI)
|
||||
self.assertBody('Hello world!')
|
||||
|
||||
def test_malformed_header(self):
|
||||
c = self.make_connection()
|
||||
c.putrequest('GET', '/')
|
||||
c.putheader('Content-Type', 'text/plain')
|
||||
# See https://github.com/cherrypy/cherrypy/issues/941
|
||||
c._output(b're, 1.2.3.4#015#012')
|
||||
c.endheaders()
|
||||
|
||||
response = c.getresponse()
|
||||
self.status = str(response.status)
|
||||
self.assertStatus(400)
|
||||
self.body = response.fp.read(20)
|
||||
self.assertBody('Illegal header line.')
|
||||
|
||||
def test_http_over_https(self):
|
||||
if self.scheme != 'https':
|
||||
return self.skip('skipped (not running HTTPS)... ')
|
||||
|
||||
# Try connecting without SSL.
|
||||
conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
conn.putrequest('GET', '/', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
try:
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 400)
|
||||
self.body = response.read()
|
||||
self.assertBody('The client sent a plain HTTP request, but this '
|
||||
'server only speaks HTTPS on this port.')
|
||||
except socket.error:
|
||||
e = sys.exc_info()[1]
|
||||
# "Connection reset by peer" is also acceptable.
|
||||
if e.errno != errno.ECONNRESET:
|
||||
raise
|
||||
|
||||
def test_garbage_in(self):
|
||||
# Connect without SSL regardless of server.scheme
|
||||
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
c._output(b'gjkgjklsgjklsgjkljklsg')
|
||||
c._send_output()
|
||||
response = c.response_class(c.sock, method='GET')
|
||||
try:
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 400)
|
||||
self.assertEqual(response.fp.read(22),
|
||||
b'Malformed Request-Line')
|
||||
c.close()
|
||||
except socket.error:
|
||||
e = sys.exc_info()[1]
|
||||
# "Connection reset by peer" is also acceptable.
|
||||
if e.errno != errno.ECONNRESET:
|
||||
raise
|
||||
80
lib/cherrypy/test/test_httputil.py
Normal file
80
lib/cherrypy/test/test_httputil.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Test helpers from ``cherrypy.lib.httputil`` module."""
|
||||
import pytest
|
||||
from six.moves import http_client
|
||||
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'script_name,path_info,expected_url',
|
||||
[
|
||||
('/sn/', '/pi/', '/sn/pi/'),
|
||||
('/sn/', '/pi', '/sn/pi'),
|
||||
('/sn/', '/', '/sn/'),
|
||||
('/sn/', '', '/sn/'),
|
||||
('/sn', '/pi/', '/sn/pi/'),
|
||||
('/sn', '/pi', '/sn/pi'),
|
||||
('/sn', '/', '/sn/'),
|
||||
('/sn', '', '/sn'),
|
||||
('/', '/pi/', '/pi/'),
|
||||
('/', '/pi', '/pi'),
|
||||
('/', '/', '/'),
|
||||
('/', '', '/'),
|
||||
('', '/pi/', '/pi/'),
|
||||
('', '/pi', '/pi'),
|
||||
('', '/', '/'),
|
||||
('', '', '/'),
|
||||
]
|
||||
)
|
||||
def test_urljoin(script_name, path_info, expected_url):
|
||||
"""Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO."""
|
||||
actual_url = httputil.urljoin(script_name, path_info)
|
||||
assert actual_url == expected_url
|
||||
|
||||
|
||||
EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows')
|
||||
EXPECTED_500 = (
|
||||
500,
|
||||
'Internal Server Error',
|
||||
'The server encountered an unexpected condition which '
|
||||
'prevented it from fulfilling the request.',
|
||||
)
|
||||
EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI')
|
||||
EXPECTED_444 = (444, 'Non-existent reason', '')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'status,expected_status',
|
||||
[
|
||||
(None, EXPECTED_200),
|
||||
(200, EXPECTED_200),
|
||||
('500', EXPECTED_500),
|
||||
(http_client.NOT_FOUND, EXPECTED_404),
|
||||
('444 Non-existent reason', EXPECTED_444),
|
||||
]
|
||||
)
|
||||
def test_valid_status(status, expected_status):
|
||||
"""Check valid int, string and http_client-constants
|
||||
statuses processing."""
|
||||
assert httputil.valid_status(status) == expected_status
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'status_code,error_msg',
|
||||
[
|
||||
('hey', "Illegal response status from server ('hey' is non-numeric)."),
|
||||
(
|
||||
{'hey': 'hi'},
|
||||
'Illegal response status from server '
|
||||
"({'hey': 'hi'} is non-numeric).",
|
||||
),
|
||||
(1, 'Illegal response status from server (1 is out of range).'),
|
||||
(600, 'Illegal response status from server (600 is out of range).'),
|
||||
]
|
||||
)
|
||||
def test_invalid_status(status_code, error_msg):
|
||||
"""Check that invalid status cause certain errors."""
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
httputil.valid_status(status_code)
|
||||
|
||||
assert error_msg in str(excinfo)
|
||||
196
lib/cherrypy/test/test_iterator.py
Normal file
196
lib/cherrypy/test/test_iterator.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class IteratorBase(object):
|
||||
|
||||
created = 0
|
||||
datachunk = 'butternut squash' * 256
|
||||
|
||||
@classmethod
|
||||
def incr(cls):
|
||||
cls.created += 1
|
||||
|
||||
@classmethod
|
||||
def decr(cls):
|
||||
cls.created -= 1
|
||||
|
||||
|
||||
class OurGenerator(IteratorBase):
|
||||
|
||||
def __iter__(self):
|
||||
self.incr()
|
||||
try:
|
||||
for i in range(1024):
|
||||
yield self.datachunk
|
||||
finally:
|
||||
self.decr()
|
||||
|
||||
|
||||
class OurIterator(IteratorBase):
|
||||
|
||||
started = False
|
||||
closed_off = False
|
||||
count = 0
|
||||
|
||||
def increment(self):
|
||||
self.incr()
|
||||
|
||||
def decrement(self):
|
||||
if not self.closed_off:
|
||||
self.closed_off = True
|
||||
self.decr()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self.started:
|
||||
self.started = True
|
||||
self.increment()
|
||||
self.count += 1
|
||||
if self.count > 1024:
|
||||
raise StopIteration
|
||||
return self.datachunk
|
||||
|
||||
next = __next__
|
||||
|
||||
def __del__(self):
|
||||
self.decrement()
|
||||
|
||||
|
||||
class OurClosableIterator(OurIterator):
|
||||
|
||||
def close(self):
|
||||
self.decrement()
|
||||
|
||||
|
||||
class OurNotClosableIterator(OurIterator):
|
||||
|
||||
# We can't close something which requires an additional argument.
|
||||
def close(self, somearg):
|
||||
self.decrement()
|
||||
|
||||
|
||||
class OurUnclosableIterator(OurIterator):
|
||||
close = 'close' # not callable!
|
||||
|
||||
|
||||
class IteratorTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
class Root(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def count(self, clsname):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
return six.text_type(globals()[clsname].created)
|
||||
|
||||
@cherrypy.expose
|
||||
def getall(self, clsname):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
return globals()[clsname]()
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
def stream(self, clsname):
|
||||
return self.getall(clsname)
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
def test_iterator(self):
|
||||
try:
|
||||
self._test_iterator()
|
||||
except Exception:
|
||||
'Test fails intermittently. See #1419'
|
||||
|
||||
def _test_iterator(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Check the counts of all the classes, they should be zero.
|
||||
closables = ['OurClosableIterator', 'OurGenerator']
|
||||
unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator']
|
||||
all_classes = closables + unclosables
|
||||
|
||||
import random
|
||||
random.shuffle(all_classes)
|
||||
|
||||
for clsname in all_classes:
|
||||
self.getPage('/count/' + clsname)
|
||||
self.assertStatus(200)
|
||||
self.assertBody('0')
|
||||
|
||||
# We should also be able to read the entire content body
|
||||
# successfully, though we don't need to, we just want to
|
||||
# check the header.
|
||||
for clsname in all_classes:
|
||||
itr_conn = self.get_conn()
|
||||
itr_conn.putrequest('GET', '/getall/' + clsname)
|
||||
itr_conn.endheaders()
|
||||
response = itr_conn.getresponse()
|
||||
self.assertEqual(response.status, 200)
|
||||
headers = response.getheaders()
|
||||
for header_name, header_value in headers:
|
||||
if header_name.lower() == 'content-length':
|
||||
expected = six.text_type(1024 * 16 * 256)
|
||||
assert header_value == expected, header_value
|
||||
break
|
||||
else:
|
||||
raise AssertionError('No Content-Length header found')
|
||||
|
||||
# As the response should be fully consumed by CherryPy
|
||||
# before sending back, the count should still be at zero
|
||||
# by the time the response has been sent.
|
||||
self.getPage('/count/' + clsname)
|
||||
self.assertStatus(200)
|
||||
self.assertBody('0')
|
||||
|
||||
# Now we do the same check with streaming - some classes will
|
||||
# be automatically closed, while others cannot.
|
||||
stream_counts = {}
|
||||
for clsname in all_classes:
|
||||
itr_conn = self.get_conn()
|
||||
itr_conn.putrequest('GET', '/stream/' + clsname)
|
||||
itr_conn.endheaders()
|
||||
response = itr_conn.getresponse()
|
||||
self.assertEqual(response.status, 200)
|
||||
response.fp.read(65536)
|
||||
|
||||
# Let's check the count - this should always be one.
|
||||
self.getPage('/count/' + clsname)
|
||||
self.assertBody('1')
|
||||
|
||||
# Now if we close the connection, the count should go back
|
||||
# to zero.
|
||||
itr_conn.close()
|
||||
self.getPage('/count/' + clsname)
|
||||
|
||||
# If this is a response which should be easily closed, then
|
||||
# we will test to see if the value has gone back down to
|
||||
# zero.
|
||||
if clsname in closables:
|
||||
|
||||
# Sometimes we try to get the answer too quickly - we
|
||||
# will wait for 100 ms before asking again if we didn't
|
||||
# get the answer we wanted.
|
||||
if self.body != '0':
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
self.getPage('/count/' + clsname)
|
||||
|
||||
stream_counts[clsname] = int(self.body)
|
||||
|
||||
# Check that we closed off the classes which should provide
|
||||
# easy mechanisms for doing so.
|
||||
for clsname in closables:
|
||||
assert stream_counts[clsname] == 0, (
|
||||
'did not close off stream response correctly, expected '
|
||||
'count of zero for %s: %s' % (clsname, stream_counts)
|
||||
)
|
||||
102
lib/cherrypy/test/test_json.py
Normal file
102
lib/cherrypy/test/test_json.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
from cherrypy._cpcompat import json
|
||||
|
||||
|
||||
json_out = cherrypy.config(**{'tools.json_out.on': True})
|
||||
json_in = cherrypy.config(**{'tools.json_in.on': True})
|
||||
|
||||
|
||||
class JsonTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def plain(self):
|
||||
return 'hello'
|
||||
|
||||
@cherrypy.expose
|
||||
@json_out
|
||||
def json_string(self):
|
||||
return 'hello'
|
||||
|
||||
@cherrypy.expose
|
||||
@json_out
|
||||
def json_list(self):
|
||||
return ['a', 'b', 42]
|
||||
|
||||
@cherrypy.expose
|
||||
@json_out
|
||||
def json_dict(self):
|
||||
return {'answer': 42}
|
||||
|
||||
@cherrypy.expose
|
||||
@json_in
|
||||
def json_post(self):
|
||||
if cherrypy.request.json == [13, 'c']:
|
||||
return 'ok'
|
||||
else:
|
||||
return 'nok'
|
||||
|
||||
@cherrypy.expose
|
||||
@json_out
|
||||
@cherrypy.config(**{'tools.caching.on': True})
|
||||
def json_cached(self):
|
||||
return 'hello there'
|
||||
|
||||
root = Root()
|
||||
cherrypy.tree.mount(root)
|
||||
|
||||
def test_json_output(self):
|
||||
if json is None:
|
||||
self.skip('json not found ')
|
||||
return
|
||||
|
||||
self.getPage('/plain')
|
||||
self.assertBody('hello')
|
||||
|
||||
self.getPage('/json_string')
|
||||
self.assertBody('"hello"')
|
||||
|
||||
self.getPage('/json_list')
|
||||
self.assertBody('["a", "b", 42]')
|
||||
|
||||
self.getPage('/json_dict')
|
||||
self.assertBody('{"answer": 42}')
|
||||
|
||||
def test_json_input(self):
|
||||
if json is None:
|
||||
self.skip('json not found ')
|
||||
return
|
||||
|
||||
body = '[13, "c"]'
|
||||
headers = [('Content-Type', 'application/json'),
|
||||
('Content-Length', str(len(body)))]
|
||||
self.getPage('/json_post', method='POST', headers=headers, body=body)
|
||||
self.assertBody('ok')
|
||||
|
||||
body = '[13, "c"]'
|
||||
headers = [('Content-Type', 'text/plain'),
|
||||
('Content-Length', str(len(body)))]
|
||||
self.getPage('/json_post', method='POST', headers=headers, body=body)
|
||||
self.assertStatus(415, 'Expected an application/json content type')
|
||||
|
||||
body = '[13, -]'
|
||||
headers = [('Content-Type', 'application/json'),
|
||||
('Content-Length', str(len(body)))]
|
||||
self.getPage('/json_post', method='POST', headers=headers, body=body)
|
||||
self.assertStatus(400, 'Invalid JSON document')
|
||||
|
||||
def test_cached(self):
|
||||
if json is None:
|
||||
self.skip('json not found ')
|
||||
return
|
||||
|
||||
self.getPage('/json_cached')
|
||||
self.assertStatus(200, '"hello"')
|
||||
|
||||
self.getPage('/json_cached') # 2'nd time to hit cache
|
||||
self.assertStatus(200, '"hello"')
|
||||
209
lib/cherrypy/test/test_logging.py
Normal file
209
lib/cherrypy/test/test_logging.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Basic tests for the CherryPy core: request handling."""
|
||||
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy.test import helper, logtest
|
||||
|
||||
localDir = os.path.dirname(__file__)
|
||||
access_log = os.path.join(localDir, 'access.log')
|
||||
error_log = os.path.join(localDir, 'error.log')
|
||||
|
||||
# Some unicode strings.
|
||||
tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
|
||||
erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
|
||||
|
||||
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'hello'
|
||||
|
||||
@cherrypy.expose
|
||||
def uni_code(self):
|
||||
cherrypy.request.login = tartaros
|
||||
cherrypy.request.remote.name = erebos
|
||||
|
||||
@cherrypy.expose
|
||||
def slashes(self):
|
||||
cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
|
||||
|
||||
@cherrypy.expose
|
||||
def whitespace(self):
|
||||
# User-Agent = "User-Agent" ":" 1*( product | comment )
|
||||
# comment = "(" *( ctext | quoted-pair | comment ) ")"
|
||||
# ctext = <any TEXT excluding "(" and ")">
|
||||
# TEXT = <any OCTET except CTLs, but including LWS>
|
||||
# LWS = [CRLF] 1*( SP | HT )
|
||||
cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
|
||||
|
||||
@cherrypy.expose
|
||||
def as_string(self):
|
||||
return 'content'
|
||||
|
||||
@cherrypy.expose
|
||||
def as_yield(self):
|
||||
yield 'content'
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.log_tracebacks.on': True})
|
||||
def error(self):
|
||||
raise ValueError()
|
||||
|
||||
root = Root()
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.error_file': error_log,
|
||||
'log.access_file': access_log,
|
||||
})
|
||||
cherrypy.tree.mount(root)
|
||||
|
||||
|
||||
class AccessLogTests(helper.CPWebCase, logtest.LogCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
logfile = access_log
|
||||
|
||||
def testNormalReturn(self):
|
||||
self.markLog()
|
||||
self.getPage('/as_string',
|
||||
headers=[('Referer', 'http://www.cherrypy.org/'),
|
||||
('User-Agent', 'Mozilla/5.0')])
|
||||
self.assertBody('content')
|
||||
self.assertStatus(200)
|
||||
|
||||
intro = '%s - - [' % self.interface()
|
||||
|
||||
self.assertLog(-1, intro)
|
||||
|
||||
if [k for k, v in self.headers if k.lower() == 'content-length']:
|
||||
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
|
||||
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
||||
% self.prefix())
|
||||
else:
|
||||
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
|
||||
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
||||
% self.prefix())
|
||||
|
||||
def testNormalYield(self):
|
||||
self.markLog()
|
||||
self.getPage('/as_yield')
|
||||
self.assertBody('content')
|
||||
self.assertStatus(200)
|
||||
|
||||
intro = '%s - - [' % self.interface()
|
||||
|
||||
self.assertLog(-1, intro)
|
||||
if [k for k, v in self.headers if k.lower() == 'content-length']:
|
||||
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
|
||||
self.prefix())
|
||||
else:
|
||||
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
|
||||
% self.prefix())
|
||||
|
||||
@mock.patch(
|
||||
'cherrypy._cplogging.LogManager.access_log_format',
|
||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
|
||||
if six.PY3 else
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
|
||||
)
|
||||
def testCustomLogFormat(self):
|
||||
"""Test a customized access_log_format string, which is a
|
||||
feature of _cplogging.LogManager.access()."""
|
||||
self.markLog()
|
||||
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
|
||||
('User-Agent', 'USERAGENT'),
|
||||
('Host', 'HOST')])
|
||||
self.assertLog(-1, '%s - - [' % self.interface())
|
||||
self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
|
||||
'200 7 "REFERER" "USERAGENT" HOST')
|
||||
|
||||
@mock.patch(
|
||||
'cherrypy._cplogging.LogManager.access_log_format',
|
||||
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
|
||||
if six.PY3 else
|
||||
'%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
|
||||
)
|
||||
def testTimezLogFormat(self):
|
||||
"""Test a customized access_log_format string, which is a
|
||||
feature of _cplogging.LogManager.access()."""
|
||||
self.markLog()
|
||||
|
||||
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
|
||||
with mock.patch(
|
||||
'cherrypy._cplogging.LazyRfc3339UtcTime',
|
||||
lambda: expected_time):
|
||||
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
|
||||
('User-Agent', 'USERAGENT'),
|
||||
('Host', 'HOST')])
|
||||
|
||||
self.assertLog(-1, '%s - - ' % self.interface())
|
||||
self.assertLog(-1, expected_time)
|
||||
self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
|
||||
'200 7 "REFERER" "USERAGENT" HOST')
|
||||
|
||||
@mock.patch(
|
||||
'cherrypy._cplogging.LogManager.access_log_format',
|
||||
'{i}' if six.PY3 else '%(i)s'
|
||||
)
|
||||
def testUUIDv4ParameterLogFormat(self):
|
||||
"""Test rendering of UUID4 within access log."""
|
||||
self.markLog()
|
||||
self.getPage('/as_string')
|
||||
self.assertValidUUIDv4()
|
||||
|
||||
def testEscapedOutput(self):
|
||||
# Test unicode in access log pieces.
|
||||
self.markLog()
|
||||
self.getPage('/uni_code')
|
||||
self.assertStatus(200)
|
||||
if six.PY3:
|
||||
# The repr of a bytestring in six.PY3 includes a b'' prefix
|
||||
self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
|
||||
else:
|
||||
self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
|
||||
# Test the erebos value. Included inline for your enlightenment.
|
||||
# Note the 'r' prefix--those backslashes are literals.
|
||||
self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
|
||||
|
||||
# Test backslashes in output.
|
||||
self.markLog()
|
||||
self.getPage('/slashes')
|
||||
self.assertStatus(200)
|
||||
if six.PY3:
|
||||
self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
|
||||
else:
|
||||
self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
|
||||
|
||||
# Test whitespace in output.
|
||||
self.markLog()
|
||||
self.getPage('/whitespace')
|
||||
self.assertStatus(200)
|
||||
# Again, note the 'r' prefix.
|
||||
self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
|
||||
|
||||
|
||||
class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
logfile = error_log
|
||||
|
||||
def testTracebacks(self):
|
||||
# Test that tracebacks get written to the error log.
|
||||
self.markLog()
|
||||
ignore = helper.webtest.ignored_exceptions
|
||||
ignore.append(ValueError)
|
||||
try:
|
||||
self.getPage('/error')
|
||||
self.assertInBody('raise ValueError()')
|
||||
self.assertLog(0, 'HTTP')
|
||||
self.assertLog(1, 'Traceback (most recent call last):')
|
||||
self.assertLog(-2, 'raise ValueError()')
|
||||
finally:
|
||||
ignore.pop()
|
||||
134
lib/cherrypy/test/test_mime.py
Normal file
134
lib/cherrypy/test/test_mime.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Tests for various MIME issues, including the safe_multipart Tool."""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
def setup_server():
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def multipart(self, parts):
|
||||
return repr(parts)
|
||||
|
||||
@cherrypy.expose
|
||||
def multipart_form_data(self, **kwargs):
|
||||
return repr(list(sorted(kwargs.items())))
|
||||
|
||||
@cherrypy.expose
|
||||
def flashupload(self, Filedata, Upload, Filename):
|
||||
return ('Upload: %s, Filename: %s, Filedata: %r' %
|
||||
(Upload, Filename, Filedata.file.read()))
|
||||
|
||||
cherrypy.config.update({'server.max_request_body_size': 0})
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
|
||||
# Client-side code #
|
||||
|
||||
|
||||
class MultipartTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_multipart(self):
|
||||
text_part = ntou('This is the text version')
|
||||
html_part = ntou(
|
||||
"""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
|
||||
</head>
|
||||
<body bgcolor="#ffffff" text="#000000">
|
||||
|
||||
This is the <strong>HTML</strong> version
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
body = '\r\n'.join([
|
||||
'--123456789',
|
||||
"Content-Type: text/plain; charset='ISO-8859-1'",
|
||||
'Content-Transfer-Encoding: 7bit',
|
||||
'',
|
||||
text_part,
|
||||
'--123456789',
|
||||
"Content-Type: text/html; charset='ISO-8859-1'",
|
||||
'',
|
||||
html_part,
|
||||
'--123456789--'])
|
||||
headers = [
|
||||
('Content-Type', 'multipart/mixed; boundary=123456789'),
|
||||
('Content-Length', str(len(body))),
|
||||
]
|
||||
self.getPage('/multipart', headers, 'POST', body)
|
||||
self.assertBody(repr([text_part, html_part]))
|
||||
|
||||
def test_multipart_form_data(self):
|
||||
body = '\r\n'.join([
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="foo"',
|
||||
'',
|
||||
'bar',
|
||||
'--X',
|
||||
# Test a param with more than one value.
|
||||
# See
|
||||
# https://github.com/cherrypy/cherrypy/issues/1028
|
||||
'Content-Disposition: form-data; name="baz"',
|
||||
'',
|
||||
'111',
|
||||
'--X',
|
||||
'Content-Disposition: form-data; name="baz"',
|
||||
'',
|
||||
'333',
|
||||
'--X--'
|
||||
])
|
||||
self.getPage('/multipart_form_data', method='POST',
|
||||
headers=[(
|
||||
'Content-Type', 'multipart/form-data;boundary=X'),
|
||||
('Content-Length', str(len(body))),
|
||||
],
|
||||
body=body),
|
||||
self.assertBody(
|
||||
repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
|
||||
|
||||
|
||||
class SafeMultipartHandlingTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_Flash_Upload(self):
|
||||
headers = [
|
||||
('Accept', 'text/*'),
|
||||
('Content-Type', 'multipart/form-data; '
|
||||
'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
|
||||
('User-Agent', 'Shockwave Flash'),
|
||||
('Host', 'www.example.com:54583'),
|
||||
('Content-Length', '499'),
|
||||
('Connection', 'Keep-Alive'),
|
||||
('Cache-Control', 'no-cache'),
|
||||
]
|
||||
filedata = (b'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||
b'<projectDescription>\r\n'
|
||||
b'</projectDescription>\r\n')
|
||||
body = (
|
||||
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
b'Content-Disposition: form-data; name="Filename"\r\n'
|
||||
b'\r\n'
|
||||
b'.project\r\n'
|
||||
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
b'Content-Disposition: form-data; '
|
||||
b'name="Filedata"; filename=".project"\r\n'
|
||||
b'Content-Type: application/octet-stream\r\n'
|
||||
b'\r\n' +
|
||||
filedata +
|
||||
b'\r\n'
|
||||
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
b'Content-Disposition: form-data; name="Upload"\r\n'
|
||||
b'\r\n'
|
||||
b'Submit Query\r\n'
|
||||
# Flash apps omit the trailing \r\n on the last line:
|
||||
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
|
||||
)
|
||||
self.getPage('/flashupload', headers, 'POST', body)
|
||||
self.assertBody('Upload: Submit Query, Filename: .project, '
|
||||
'Filedata: %r' % filedata)
|
||||
210
lib/cherrypy/test/test_misc_tools.py
Normal file
210
lib/cherrypy/test/test_misc_tools.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import os
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import tools
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
localDir = os.path.dirname(__file__)
|
||||
logfile = os.path.join(localDir, 'test_misc_tools.log')
|
||||
|
||||
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
yield 'Hello, world'
|
||||
h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')]
|
||||
tools.response_headers(headers=h)(index)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.response_headers.on': True,
|
||||
'tools.response_headers.headers': [
|
||||
('Content-Language', 'fr'),
|
||||
('Content-Type', 'text/plain'),
|
||||
],
|
||||
'tools.log_hooks.on': True,
|
||||
})
|
||||
def other(self):
|
||||
return 'salut'
|
||||
|
||||
@cherrypy.config(**{'tools.accept.on': True})
|
||||
class Accept:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return '<a href="feed">Atom feed</a>'
|
||||
|
||||
@cherrypy.expose
|
||||
@tools.accept(media='application/atom+xml')
|
||||
def feed(self):
|
||||
return """<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Unknown Blog</title>
|
||||
</feed>"""
|
||||
|
||||
@cherrypy.expose
|
||||
def select(self):
|
||||
# We could also write this: mtype = cherrypy.lib.accept.accept(...)
|
||||
mtype = tools.accept.callable(['text/html', 'text/plain'])
|
||||
if mtype == 'text/html':
|
||||
return '<h2>Page Title</h2>'
|
||||
else:
|
||||
return 'PAGE TITLE'
|
||||
|
||||
class Referer:
|
||||
|
||||
@cherrypy.expose
|
||||
def accept(self):
|
||||
return 'Accepted!'
|
||||
reject = accept
|
||||
|
||||
class AutoVary:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
# Read a header directly with 'get'
|
||||
cherrypy.request.headers.get('Accept-Encoding')
|
||||
# Read a header directly with '__getitem__'
|
||||
cherrypy.request.headers['Host']
|
||||
# Read a header directly with '__contains__'
|
||||
'If-Modified-Since' in cherrypy.request.headers
|
||||
# Read a header directly
|
||||
'Range' in cherrypy.request.headers
|
||||
# Call a lib function
|
||||
tools.accept.callable(['text/html', 'text/plain'])
|
||||
return 'Hello, world!'
|
||||
|
||||
conf = {'/referer': {'tools.referer.on': True,
|
||||
'tools.referer.pattern': r'http://[^/]*example\.com',
|
||||
},
|
||||
'/referer/reject': {'tools.referer.accept': False,
|
||||
'tools.referer.accept_missing': True,
|
||||
},
|
||||
'/autovary': {'tools.autovary.on': True},
|
||||
}
|
||||
|
||||
root = Root()
|
||||
root.referer = Referer()
|
||||
root.accept = Accept()
|
||||
root.autovary = AutoVary()
|
||||
cherrypy.tree.mount(root, config=conf)
|
||||
cherrypy.config.update({'log.error_file': logfile})
|
||||
|
||||
|
||||
class ResponseHeadersTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testResponseHeadersDecorator(self):
|
||||
self.getPage('/')
|
||||
self.assertHeader('Content-Language', 'en-GB')
|
||||
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
|
||||
|
||||
def testResponseHeaders(self):
|
||||
self.getPage('/other')
|
||||
self.assertHeader('Content-Language', 'fr')
|
||||
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
|
||||
|
||||
|
||||
class RefererTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testReferer(self):
|
||||
self.getPage('/referer/accept')
|
||||
self.assertErrorPage(403, 'Forbidden Referer header.')
|
||||
|
||||
self.getPage('/referer/accept',
|
||||
headers=[('Referer', 'http://www.example.com/')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Accepted!')
|
||||
|
||||
# Reject
|
||||
self.getPage('/referer/reject')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('Accepted!')
|
||||
|
||||
self.getPage('/referer/reject',
|
||||
headers=[('Referer', 'http://www.example.com/')])
|
||||
self.assertErrorPage(403, 'Forbidden Referer header.')
|
||||
|
||||
|
||||
class AcceptTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_Accept_Tool(self):
|
||||
# Test with no header provided
|
||||
self.getPage('/accept/feed')
|
||||
self.assertStatus(200)
|
||||
self.assertInBody('<title>Unknown Blog</title>')
|
||||
|
||||
# Specify exact media type
|
||||
self.getPage('/accept/feed',
|
||||
headers=[('Accept', 'application/atom+xml')])
|
||||
self.assertStatus(200)
|
||||
self.assertInBody('<title>Unknown Blog</title>')
|
||||
|
||||
# Specify matching media range
|
||||
self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
|
||||
self.assertStatus(200)
|
||||
self.assertInBody('<title>Unknown Blog</title>')
|
||||
|
||||
# Specify all media ranges
|
||||
self.getPage('/accept/feed', headers=[('Accept', '*/*')])
|
||||
self.assertStatus(200)
|
||||
self.assertInBody('<title>Unknown Blog</title>')
|
||||
|
||||
# Specify unacceptable media types
|
||||
self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
|
||||
self.assertErrorPage(406,
|
||||
'Your client sent this Accept header: text/html. '
|
||||
'But this resource only emits these media types: '
|
||||
'application/atom+xml.')
|
||||
|
||||
# Test resource where tool is 'on' but media is None (not set).
|
||||
self.getPage('/accept/')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('<a href="feed">Atom feed</a>')
|
||||
|
||||
def test_accept_selection(self):
|
||||
# Try both our expected media types
|
||||
self.getPage('/accept/select', [('Accept', 'text/html')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('<h2>Page Title</h2>')
|
||||
self.getPage('/accept/select', [('Accept', 'text/plain')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('PAGE TITLE')
|
||||
self.getPage('/accept/select',
|
||||
[('Accept', 'text/plain, text/*;q=0.5')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('PAGE TITLE')
|
||||
|
||||
# text/* and */* should prefer text/html since it comes first
|
||||
# in our 'media' argument to tools.accept
|
||||
self.getPage('/accept/select', [('Accept', 'text/*')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('<h2>Page Title</h2>')
|
||||
self.getPage('/accept/select', [('Accept', '*/*')])
|
||||
self.assertStatus(200)
|
||||
self.assertBody('<h2>Page Title</h2>')
|
||||
|
||||
# Try unacceptable media types
|
||||
self.getPage('/accept/select', [('Accept', 'application/xml')])
|
||||
self.assertErrorPage(
|
||||
406,
|
||||
'Your client sent this Accept header: application/xml. '
|
||||
'But this resource only emits these media types: '
|
||||
'text/html, text/plain.')
|
||||
|
||||
|
||||
class AutoVaryTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testAutoVary(self):
|
||||
self.getPage('/autovary/')
|
||||
self.assertHeader(
|
||||
'Vary',
|
||||
'Accept, Accept-Charset, Accept-Encoding, '
|
||||
'Host, If-Modified-Since, Range'
|
||||
)
|
||||
38
lib/cherrypy/test/test_native.py
Normal file
38
lib/cherrypy/test/test_native.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Test the native server."""
|
||||
|
||||
import pytest
|
||||
from requests_toolbelt import sessions
|
||||
|
||||
import cherrypy._cpnative_server
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
'sys.platform == "win32"',
|
||||
reason='tests fail on Windows',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cp_native_server(request):
|
||||
"""A native server."""
|
||||
class Root(object):
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hello World!'
|
||||
|
||||
cls = cherrypy._cpnative_server.CPHTTPServer
|
||||
cherrypy.server.httpserver = cls(cherrypy.server)
|
||||
|
||||
cherrypy.tree.mount(Root(), '/')
|
||||
cherrypy.engine.start()
|
||||
request.addfinalizer(cherrypy.engine.stop)
|
||||
url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals())
|
||||
return sessions.BaseUrlSession(url)
|
||||
|
||||
|
||||
def test_basic_request(cp_native_server):
|
||||
"""A request to a native server should succeed."""
|
||||
resp = cp_native_server.get('/')
|
||||
assert resp.ok
|
||||
assert resp.status_code == 200
|
||||
assert resp.text == 'Hello World!'
|
||||
430
lib/cherrypy/test/test_objectmapping.py
Normal file
430
lib/cherrypy/test/test_objectmapping.py
Normal file
@@ -0,0 +1,430 @@
|
||||
import sys
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy._cptree import Application
|
||||
from cherrypy.test import helper
|
||||
|
||||
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
|
||||
|
||||
|
||||
class ObjectMappingTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, name='world'):
|
||||
return name
|
||||
|
||||
@cherrypy.expose
|
||||
def foobar(self):
|
||||
return 'bar'
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *params, **kwargs):
|
||||
return 'default:' + repr(params)
|
||||
|
||||
@cherrypy.expose
|
||||
def other(self):
|
||||
return 'other'
|
||||
|
||||
@cherrypy.expose
|
||||
def extra(self, *p):
|
||||
return repr(p)
|
||||
|
||||
@cherrypy.expose
|
||||
def redirect(self):
|
||||
raise cherrypy.HTTPRedirect('dir1/', 302)
|
||||
|
||||
def notExposed(self):
|
||||
return 'not exposed'
|
||||
|
||||
@cherrypy.expose
|
||||
def confvalue(self):
|
||||
return cherrypy.request.config.get('user')
|
||||
|
||||
@cherrypy.expose
|
||||
def redirect_via_url(self, path):
|
||||
raise cherrypy.HTTPRedirect(cherrypy.url(path))
|
||||
|
||||
@cherrypy.expose
|
||||
def translate_html(self):
|
||||
return 'OK'
|
||||
|
||||
@cherrypy.expose
|
||||
def mapped_func(self, ID=None):
|
||||
return 'ID is %s' % ID
|
||||
setattr(Root, 'Von B\xfclow', mapped_func)
|
||||
|
||||
class Exposing:
|
||||
|
||||
@cherrypy.expose
|
||||
def base(self):
|
||||
return 'expose works!'
|
||||
cherrypy.expose(base, '1')
|
||||
cherrypy.expose(base, '2')
|
||||
|
||||
class ExposingNewStyle(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def base(self):
|
||||
return 'expose works!'
|
||||
cherrypy.expose(base, '1')
|
||||
cherrypy.expose(base, '2')
|
||||
|
||||
class Dir1:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'index for dir1'
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.trailing_slash.extra': True})
|
||||
def myMethod(self):
|
||||
return 'myMethod from dir1, path_info is:' + repr(
|
||||
cherrypy.request.path_info)
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *params):
|
||||
return 'default for dir1, param is:' + repr(params)
|
||||
|
||||
class Dir2:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'index for dir2, path is:' + cherrypy.request.path_info
|
||||
|
||||
@cherrypy.expose
|
||||
def script_name(self):
|
||||
return cherrypy.tree.script_name()
|
||||
|
||||
@cherrypy.expose
|
||||
def cherrypy_url(self):
|
||||
return cherrypy.url('/extra')
|
||||
|
||||
@cherrypy.expose
|
||||
def posparam(self, *vpath):
|
||||
return '/'.join(vpath)
|
||||
|
||||
class Dir3:
|
||||
|
||||
def default(self):
|
||||
return 'default for dir3, not exposed'
|
||||
|
||||
class Dir4:
|
||||
|
||||
def index(self):
|
||||
return 'index for dir4, not exposed'
|
||||
|
||||
class DefNoIndex:
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
raise cherrypy.HTTPRedirect('contact')
|
||||
|
||||
# MethodDispatcher code
|
||||
@cherrypy.expose
|
||||
class ByMethod:
|
||||
|
||||
def __init__(self, *things):
|
||||
self.things = list(things)
|
||||
|
||||
def GET(self):
|
||||
return repr(self.things)
|
||||
|
||||
def POST(self, thing):
|
||||
self.things.append(thing)
|
||||
|
||||
class Collection:
|
||||
default = ByMethod('a', 'bit')
|
||||
|
||||
Root.exposing = Exposing()
|
||||
Root.exposingnew = ExposingNewStyle()
|
||||
Root.dir1 = Dir1()
|
||||
Root.dir1.dir2 = Dir2()
|
||||
Root.dir1.dir2.dir3 = Dir3()
|
||||
Root.dir1.dir2.dir3.dir4 = Dir4()
|
||||
Root.defnoindex = DefNoIndex()
|
||||
Root.bymethod = ByMethod('another')
|
||||
Root.collection = Collection()
|
||||
|
||||
d = cherrypy.dispatch.MethodDispatcher()
|
||||
for url in script_names:
|
||||
conf = {'/': {'user': (url or '/').split('/')[-2]},
|
||||
'/bymethod': {'request.dispatch': d},
|
||||
'/collection': {'request.dispatch': d},
|
||||
}
|
||||
cherrypy.tree.mount(Root(), url, conf)
|
||||
|
||||
class Isolated:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'made it!'
|
||||
|
||||
cherrypy.tree.mount(Isolated(), '/isolated')
|
||||
|
||||
@cherrypy.expose
|
||||
class AnotherApp:
|
||||
|
||||
def GET(self):
|
||||
return 'milk'
|
||||
|
||||
cherrypy.tree.mount(AnotherApp(), '/app',
|
||||
{'/': {'request.dispatch': d}})
|
||||
|
||||
def testObjectMapping(self):
|
||||
for url in script_names:
|
||||
self.script_name = url
|
||||
|
||||
self.getPage('/')
|
||||
self.assertBody('world')
|
||||
|
||||
self.getPage('/dir1/myMethod')
|
||||
self.assertBody(
|
||||
"myMethod from dir1, path_info is:'/dir1/myMethod'")
|
||||
|
||||
self.getPage('/this/method/does/not/exist')
|
||||
self.assertBody(
|
||||
"default:('this', 'method', 'does', 'not', 'exist')")
|
||||
|
||||
self.getPage('/extra/too/much')
|
||||
self.assertBody("('too', 'much')")
|
||||
|
||||
self.getPage('/other')
|
||||
self.assertBody('other')
|
||||
|
||||
self.getPage('/notExposed')
|
||||
self.assertBody("default:('notExposed',)")
|
||||
|
||||
self.getPage('/dir1/dir2/')
|
||||
self.assertBody('index for dir2, path is:/dir1/dir2/')
|
||||
|
||||
# Test omitted trailing slash (should be redirected by default).
|
||||
self.getPage('/dir1/dir2')
|
||||
self.assertStatus(301)
|
||||
self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
|
||||
|
||||
# Test extra trailing slash (should be redirected if configured).
|
||||
self.getPage('/dir1/myMethod/')
|
||||
self.assertStatus(301)
|
||||
self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
|
||||
|
||||
# Test that default method must be exposed in order to match.
|
||||
self.getPage('/dir1/dir2/dir3/dir4/index')
|
||||
self.assertBody(
|
||||
"default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
|
||||
|
||||
# Test *vpath when default() is defined but not index()
|
||||
# This also tests HTTPRedirect with default.
|
||||
self.getPage('/defnoindex')
|
||||
self.assertStatus((302, 303))
|
||||
self.assertHeader('Location', '%s/contact' % self.base())
|
||||
self.getPage('/defnoindex/')
|
||||
self.assertStatus((302, 303))
|
||||
self.assertHeader('Location', '%s/defnoindex/contact' %
|
||||
self.base())
|
||||
self.getPage('/defnoindex/page')
|
||||
self.assertStatus((302, 303))
|
||||
self.assertHeader('Location', '%s/defnoindex/contact' %
|
||||
self.base())
|
||||
|
||||
self.getPage('/redirect')
|
||||
self.assertStatus('302 Found')
|
||||
self.assertHeader('Location', '%s/dir1/' % self.base())
|
||||
|
||||
if not getattr(cherrypy.server, 'using_apache', False):
|
||||
# Test that we can use URL's which aren't all valid Python
|
||||
# identifiers
|
||||
# This should also test the %XX-unquoting of URL's.
|
||||
self.getPage('/Von%20B%fclow?ID=14')
|
||||
self.assertBody('ID is 14')
|
||||
|
||||
# Test that %2F in the path doesn't get unquoted too early;
|
||||
# that is, it should not be used to separate path components.
|
||||
# See ticket #393.
|
||||
self.getPage('/page%2Fname')
|
||||
self.assertBody("default:('page/name',)")
|
||||
|
||||
self.getPage('/dir1/dir2/script_name')
|
||||
self.assertBody(url)
|
||||
self.getPage('/dir1/dir2/cherrypy_url')
|
||||
self.assertBody('%s/extra' % self.base())
|
||||
|
||||
# Test that configs don't overwrite each other from different apps
|
||||
self.getPage('/confvalue')
|
||||
self.assertBody((url or '/').split('/')[-2])
|
||||
|
||||
self.script_name = ''
|
||||
|
||||
# Test absoluteURI's in the Request-Line
|
||||
self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
|
||||
self.assertBody('world')
|
||||
|
||||
self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
|
||||
(self.interface(), self.PORT))
|
||||
self.assertBody("default:('abs',)")
|
||||
|
||||
self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
|
||||
self.assertBody("default:('rel',)")
|
||||
|
||||
# Test that the "isolated" app doesn't leak url's into the root app.
|
||||
# If it did leak, Root.default() would answer with
|
||||
# "default:('isolated', 'doesnt', 'exist')".
|
||||
self.getPage('/isolated/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('made it!')
|
||||
self.getPage('/isolated/doesnt/exist')
|
||||
self.assertStatus('404 Not Found')
|
||||
|
||||
# Make sure /foobar maps to Root.foobar and not to the app
|
||||
# mounted at /foo. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/573
|
||||
self.getPage('/foobar')
|
||||
self.assertBody('bar')
|
||||
|
||||
def test_translate(self):
|
||||
self.getPage('/translate_html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('OK')
|
||||
|
||||
self.getPage('/translate.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('OK')
|
||||
|
||||
self.getPage('/translate-html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('OK')
|
||||
|
||||
def test_redir_using_url(self):
|
||||
for url in script_names:
|
||||
self.script_name = url
|
||||
|
||||
# Test the absolute path to the parent (leading slash)
|
||||
self.getPage('/redirect_via_url?path=./')
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
self.assertHeader('Location', '%s/' % self.base())
|
||||
|
||||
# Test the relative path to the parent (no leading slash)
|
||||
self.getPage('/redirect_via_url?path=./')
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
self.assertHeader('Location', '%s/' % self.base())
|
||||
|
||||
# Test the absolute path to the parent (leading slash)
|
||||
self.getPage('/redirect_via_url/?path=./')
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
self.assertHeader('Location', '%s/' % self.base())
|
||||
|
||||
# Test the relative path to the parent (no leading slash)
|
||||
self.getPage('/redirect_via_url/?path=./')
|
||||
self.assertStatus(('302 Found', '303 See Other'))
|
||||
self.assertHeader('Location', '%s/' % self.base())
|
||||
|
||||
def testPositionalParams(self):
|
||||
self.getPage('/dir1/dir2/posparam/18/24/hut/hike')
|
||||
self.assertBody('18/24/hut/hike')
|
||||
|
||||
# intermediate index methods should not receive posparams;
|
||||
# only the "final" index method should do so.
|
||||
self.getPage('/dir1/dir2/5/3/sir')
|
||||
self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
|
||||
|
||||
# test that extra positional args raises an 404 Not Found
|
||||
# See https://github.com/cherrypy/cherrypy/issues/733.
|
||||
self.getPage('/dir1/dir2/script_name/extra/stuff')
|
||||
self.assertStatus(404)
|
||||
|
||||
def testExpose(self):
|
||||
# Test the cherrypy.expose function/decorator
|
||||
self.getPage('/exposing/base')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
self.getPage('/exposing/1')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
self.getPage('/exposing/2')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
self.getPage('/exposingnew/base')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
self.getPage('/exposingnew/1')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
self.getPage('/exposingnew/2')
|
||||
self.assertBody('expose works!')
|
||||
|
||||
def testMethodDispatch(self):
|
||||
self.getPage('/bymethod')
|
||||
self.assertBody("['another']")
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
self.getPage('/bymethod', method='HEAD')
|
||||
self.assertBody('')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
self.getPage('/bymethod', method='POST', body='thing=one')
|
||||
self.assertBody('')
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
self.getPage('/bymethod')
|
||||
self.assertBody(repr(['another', ntou('one')]))
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
self.getPage('/bymethod', method='PUT')
|
||||
self.assertErrorPage(405)
|
||||
self.assertHeader('Allow', 'GET, HEAD, POST')
|
||||
|
||||
# Test default with posparams
|
||||
self.getPage('/collection/silly', method='POST')
|
||||
self.getPage('/collection', method='GET')
|
||||
self.assertBody("['a', 'bit', 'silly']")
|
||||
|
||||
# Test custom dispatcher set on app root (see #737).
|
||||
self.getPage('/app')
|
||||
self.assertBody('milk')
|
||||
|
||||
def testTreeMounting(self):
|
||||
class Root(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def hello(self):
|
||||
return 'Hello world!'
|
||||
|
||||
# When mounting an application instance,
|
||||
# we can't specify a different script name in the call to mount.
|
||||
a = Application(Root(), '/somewhere')
|
||||
self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
|
||||
|
||||
# When mounting an application instance...
|
||||
a = Application(Root(), '/somewhere')
|
||||
# ...we MUST allow in identical script name in the call to mount...
|
||||
cherrypy.tree.mount(a, '/somewhere')
|
||||
self.getPage('/somewhere/hello')
|
||||
self.assertStatus(200)
|
||||
# ...and MUST allow a missing script_name.
|
||||
del cherrypy.tree.apps['/somewhere']
|
||||
cherrypy.tree.mount(a)
|
||||
self.getPage('/somewhere/hello')
|
||||
self.assertStatus(200)
|
||||
|
||||
# In addition, we MUST be able to create an Application using
|
||||
# script_name == None for access to the wsgi_environ.
|
||||
a = Application(Root(), script_name=None)
|
||||
# However, this does not apply to tree.mount
|
||||
self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
|
||||
|
||||
def testKeywords(self):
|
||||
if sys.version_info < (3,):
|
||||
return self.skip('skipped (Python 3 only)')
|
||||
exec("""class Root(object):
|
||||
@cherrypy.expose
|
||||
def hello(self, *, name='world'):
|
||||
return 'Hello %s!' % name
|
||||
cherrypy.tree.mount(Application(Root(), '/keywords'))""")
|
||||
|
||||
self.getPage('/keywords/hello')
|
||||
self.assertStatus(200)
|
||||
self.getPage('/keywords/hello/extra')
|
||||
self.assertStatus(404)
|
||||
61
lib/cherrypy/test/test_params.py
Normal file
61
lib/cherrypy/test/test_params.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class ParamsTest(helper.CPWebCase):
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@cherrypy.tools.params()
|
||||
def resource(self, limit=None, sort=None):
|
||||
return type(limit).__name__
|
||||
# for testing on Py 2
|
||||
resource.__annotations__ = {'limit': int}
|
||||
conf = {'/': {'tools.params.on': True}}
|
||||
cherrypy.tree.mount(Root(), config=conf)
|
||||
|
||||
def test_pass(self):
|
||||
self.getPage('/resource')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('"NoneType"')
|
||||
|
||||
self.getPage('/resource?limit=0')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('"int"')
|
||||
|
||||
def test_error(self):
|
||||
self.getPage('/resource?limit=')
|
||||
self.assertStatus(400)
|
||||
self.assertInBody('invalid literal for int')
|
||||
|
||||
cherrypy.config['tools.params.error'] = 422
|
||||
self.getPage('/resource?limit=')
|
||||
self.assertStatus(422)
|
||||
self.assertInBody('invalid literal for int')
|
||||
|
||||
cherrypy.config['tools.params.exception'] = TypeError
|
||||
self.getPage('/resource?limit=')
|
||||
self.assertStatus(500)
|
||||
|
||||
def test_syntax(self):
|
||||
if sys.version_info < (3,):
|
||||
return self.skip('skipped (Python 3 only)')
|
||||
code = textwrap.dedent("""
|
||||
class Root:
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.params()
|
||||
def resource(self, limit: int):
|
||||
return type(limit).__name__
|
||||
conf = {'/': {'tools.params.on': True}}
|
||||
cherrypy.tree.mount(Root(), config=conf)
|
||||
""")
|
||||
exec(code)
|
||||
|
||||
self.getPage('/resource?limit=0')
|
||||
self.assertStatus(200)
|
||||
self.assertBody('int')
|
||||
14
lib/cherrypy/test/test_plugins.py
Normal file
14
lib/cherrypy/test/test_plugins.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from cherrypy.process import plugins
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class TestAutoreloader:
|
||||
def test_file_for_file_module_when_None(self):
|
||||
"""No error when module.__file__ is None.
|
||||
"""
|
||||
class test_module:
|
||||
__file__ = None
|
||||
|
||||
assert plugins.Autoreloader._file_for_file_module(test_module) is None
|
||||
154
lib/cherrypy/test/test_proxy.py
Normal file
154
lib/cherrypy/test/test_proxy.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
script_names = ['', '/path/to/myapp']
|
||||
|
||||
|
||||
class ProxyTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
# Set up site
|
||||
cherrypy.config.update({
|
||||
'tools.proxy.on': True,
|
||||
'tools.proxy.base': 'www.mydomain.test',
|
||||
})
|
||||
|
||||
# Set up application
|
||||
|
||||
class Root:
|
||||
|
||||
def __init__(self, sn):
|
||||
# Calculate a URL outside of any requests.
|
||||
self.thisnewpage = cherrypy.url(
|
||||
'/this/new/page', script_name=sn)
|
||||
|
||||
@cherrypy.expose
|
||||
def pageurl(self):
|
||||
return self.thisnewpage
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise cherrypy.HTTPRedirect('dummy')
|
||||
|
||||
@cherrypy.expose
|
||||
def remoteip(self):
|
||||
return cherrypy.request.remote.ip
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.proxy.local': 'X-Host',
|
||||
'tools.trailing_slash.extra': True,
|
||||
})
|
||||
def xhost(self):
|
||||
raise cherrypy.HTTPRedirect('blah')
|
||||
|
||||
@cherrypy.expose
|
||||
def base(self):
|
||||
return cherrypy.request.base
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'})
|
||||
def ssl(self):
|
||||
return cherrypy.request.base
|
||||
|
||||
@cherrypy.expose
|
||||
def newurl(self):
|
||||
return ("Browse to <a href='%s'>this page</a>."
|
||||
% cherrypy.url('/this/new/page'))
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.proxy.base': None,
|
||||
})
|
||||
def base_no_base(self):
|
||||
return cherrypy.request.base
|
||||
|
||||
for sn in script_names:
|
||||
cherrypy.tree.mount(Root(sn), sn)
|
||||
|
||||
def testProxy(self):
|
||||
self.getPage('/')
|
||||
self.assertHeader('Location',
|
||||
'%s://www.mydomain.test%s/dummy' %
|
||||
(self.scheme, self.prefix()))
|
||||
|
||||
# Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
|
||||
self.getPage(
|
||||
'/', headers=[('X-Forwarded-Host', 'http://www.example.test')])
|
||||
self.assertHeader('Location', 'http://www.example.test/dummy')
|
||||
self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')])
|
||||
self.assertHeader('Location', '%s://www.example.test/dummy' %
|
||||
self.scheme)
|
||||
# Test multiple X-Forwarded-Host headers
|
||||
self.getPage('/', headers=[
|
||||
('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
|
||||
])
|
||||
self.assertHeader('Location', 'http://www.example.test/dummy')
|
||||
|
||||
# Test X-Forwarded-For (Apache2)
|
||||
self.getPage('/remoteip',
|
||||
headers=[('X-Forwarded-For', '192.168.0.20')])
|
||||
self.assertBody('192.168.0.20')
|
||||
# Fix bug #1268
|
||||
self.getPage('/remoteip',
|
||||
headers=[
|
||||
('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
|
||||
])
|
||||
self.assertBody('67.15.36.43')
|
||||
|
||||
# Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
|
||||
self.getPage('/xhost', headers=[('X-Host', 'www.example.test')])
|
||||
self.assertHeader('Location', '%s://www.example.test/blah' %
|
||||
self.scheme)
|
||||
|
||||
# Test X-Forwarded-Proto (lighttpd)
|
||||
self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')])
|
||||
self.assertBody('https://www.mydomain.test')
|
||||
|
||||
# Test X-Forwarded-Ssl (webfaction?)
|
||||
self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')])
|
||||
self.assertBody('https://www.mydomain.test')
|
||||
|
||||
# Test cherrypy.url()
|
||||
for sn in script_names:
|
||||
# Test the value inside requests
|
||||
self.getPage(sn + '/newurl')
|
||||
self.assertBody(
|
||||
"Browse to <a href='%s://www.mydomain.test" % self.scheme +
|
||||
sn + "/this/new/page'>this page</a>.")
|
||||
self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host',
|
||||
'http://www.example.test')])
|
||||
self.assertBody("Browse to <a href='http://www.example.test" +
|
||||
sn + "/this/new/page'>this page</a>.")
|
||||
|
||||
# Test the value outside requests
|
||||
port = ''
|
||||
if self.scheme == 'http' and self.PORT != 80:
|
||||
port = ':%s' % self.PORT
|
||||
elif self.scheme == 'https' and self.PORT != 443:
|
||||
port = ':%s' % self.PORT
|
||||
host = self.HOST
|
||||
if host in ('0.0.0.0', '::'):
|
||||
import socket
|
||||
host = socket.gethostname()
|
||||
expected = ('%s://%s%s%s/this/new/page'
|
||||
% (self.scheme, host, port, sn))
|
||||
self.getPage(sn + '/pageurl')
|
||||
self.assertBody(expected)
|
||||
|
||||
# Test trailing slash (see
|
||||
# https://github.com/cherrypy/cherrypy/issues/562).
|
||||
self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')])
|
||||
self.assertHeader('Location', '%s://www.example.test/xhost'
|
||||
% self.scheme)
|
||||
|
||||
def test_no_base_port_in_host(self):
|
||||
"""
|
||||
If no base is indicated, and the host header is used to resolve
|
||||
the base, it should rely on the host header for the port also.
|
||||
"""
|
||||
headers = {'Host': 'localhost:8080'}.items()
|
||||
self.getPage('/base_no_base', headers=headers)
|
||||
self.assertBody('http://localhost:8080')
|
||||
66
lib/cherrypy/test/test_refleaks.py
Normal file
66
lib/cherrypy/test/test_refleaks.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Tests for refleaks."""
|
||||
|
||||
import itertools
|
||||
import platform
|
||||
import threading
|
||||
|
||||
from six.moves.http_client import HTTPConnection
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import HTTPSConnection
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
data = object()
|
||||
|
||||
|
||||
class ReferenceTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, *args, **kwargs):
|
||||
cherrypy.request.thing = data
|
||||
return 'Hello world!'
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
def test_threadlocal_garbage(self):
|
||||
if platform.system() == 'Darwin':
|
||||
self.skip('queue issues; see #1474')
|
||||
success = itertools.count()
|
||||
|
||||
def getpage():
|
||||
host = '%s:%s' % (self.interface(), self.PORT)
|
||||
if self.scheme == 'https':
|
||||
c = HTTPSConnection(host)
|
||||
else:
|
||||
c = HTTPConnection(host)
|
||||
try:
|
||||
c.putrequest('GET', '/')
|
||||
c.endheaders()
|
||||
response = c.getresponse()
|
||||
body = response.read()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, b'Hello world!')
|
||||
finally:
|
||||
c.close()
|
||||
next(success)
|
||||
|
||||
ITERATIONS = 25
|
||||
|
||||
ts = [
|
||||
threading.Thread(target=getpage)
|
||||
for _ in range(ITERATIONS)
|
||||
]
|
||||
|
||||
for t in ts:
|
||||
t.start()
|
||||
|
||||
for t in ts:
|
||||
t.join()
|
||||
|
||||
self.assertEqual(next(success), ITERATIONS)
|
||||
932
lib/cherrypy/test/test_request_obj.py
Normal file
932
lib/cherrypy/test/test_request_obj.py
Normal file
@@ -0,0 +1,932 @@
|
||||
"""Basic tests for the cherrypy.Request object."""
|
||||
|
||||
from functools import wraps
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
|
||||
import six
|
||||
from six.moves.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 #
|
||||
|
||||
|
||||
class RequestObjectTests(helper.CPWebCase):
|
||||
|
||||
@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, six.text_type)
|
||||
cherrypy.response.headers['ETag'] = val
|
||||
return val
|
||||
|
||||
class HeaderElements(Test):
|
||||
|
||||
def get_elements(self, headername):
|
||||
e = cherrypy.request.headers.elements(headername)
|
||||
return '\n'.join([six.text_type(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)
|
||||
|
||||
def test_scheme(self):
|
||||
self.getPage('/scheme')
|
||||
self.assertBody(self.scheme)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
def testRelativeURIPathInfo(self):
|
||||
self.getPage('/pathinfo/foo/bar')
|
||||
self.assertBody('/pathinfo/foo/bar')
|
||||
|
||||
def testAbsoluteURIPathInfo(self):
|
||||
# http://cherrypy.org/ticket/1061
|
||||
self.getPage('http://localhost/pathinfo/foo/bar')
|
||||
self.assertBody('/pathinfo/foo/bar')
|
||||
|
||||
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¬athing=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¬athing=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.org/index%3Fa%3D1%26b%3D2')
|
||||
self.assertBody('args: %s kwargs: %s' %
|
||||
(('code',),
|
||||
[('url', ntou('http://cherrypy.org/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'))]))
|
||||
|
||||
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¶m2=bar',
|
||||
'/paramerrors/one_positional_args_kwargs/foo?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_kwargs?'
|
||||
'param1=foo¶m2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_kwargs/foo?'
|
||||
'param4=foo¶m2=bar¶m3=baz',
|
||||
'/paramerrors/no_positional',
|
||||
'/paramerrors/no_positional_args/foo',
|
||||
'/paramerrors/no_positional_args/foo/bar/baz',
|
||||
'/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar',
|
||||
'/paramerrors/no_positional_args_kwargs/foo?param2=bar',
|
||||
'/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/no_positional_kwargs?param1=foo¶m2=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¶m2=foo',
|
||||
error_msgs[2]),
|
||||
('/paramerrors/one_positional_args/foo?param1=foo¶m2=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¶m3=baz',
|
||||
error_msgs[2]),
|
||||
('/paramerrors/one_positional_kwargs/foo?'
|
||||
'param1=foo¶m2=bar¶m3=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¶m2=foo', error_msgs[2]),
|
||||
('/paramerrors/one_positional_args/foo',
|
||||
'param1=foo¶m2=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¶m3=baz', error_msgs[2]),
|
||||
('/paramerrors/one_positional_kwargs/foo',
|
||||
'param1=foo¶m2=bar¶m3=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¶m3=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)
|
||||
|
||||
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, <b>really</b>, 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()')
|
||||
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def testEmptyThreadlocals(self):
|
||||
results = []
|
||||
for x in range(20):
|
||||
self.getPage('/threadlocal/')
|
||||
results.append(self.body)
|
||||
self.assertEqual(results, [b'None'] * 20)
|
||||
80
lib/cherrypy/test/test_routes.py
Normal file
80
lib/cherrypy/test/test_routes.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Test Routes dispatcher."""
|
||||
import os
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
class RoutesDispatchTest(helper.CPWebCase):
|
||||
"""Routes dispatcher test suite."""
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
"""Set up cherrypy test instance."""
|
||||
try:
|
||||
importlib.import_module('routes')
|
||||
except ImportError:
|
||||
pytest.skip('Install routes to test RoutesDispatcher code')
|
||||
|
||||
class Dummy:
|
||||
|
||||
def index(self):
|
||||
return 'I said good day!'
|
||||
|
||||
class City:
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.population = 10000
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.response_headers.on': True,
|
||||
'tools.response_headers.headers': [
|
||||
('Content-Language', 'en-GB'),
|
||||
],
|
||||
})
|
||||
def index(self, **kwargs):
|
||||
return 'Welcome to %s, pop. %s' % (self.name, self.population)
|
||||
|
||||
def update(self, **kwargs):
|
||||
self.population = kwargs['pop']
|
||||
return 'OK'
|
||||
|
||||
d = cherrypy.dispatch.RoutesDispatcher()
|
||||
d.connect(action='index', name='hounslow', route='/hounslow',
|
||||
controller=City('Hounslow'))
|
||||
d.connect(
|
||||
name='surbiton', route='/surbiton', controller=City('Surbiton'),
|
||||
action='index', conditions=dict(method=['GET']))
|
||||
d.mapper.connect('/surbiton', controller='surbiton',
|
||||
action='update', conditions=dict(method=['POST']))
|
||||
d.connect('main', ':action', controller=Dummy())
|
||||
|
||||
conf = {'/': {'request.dispatch': d}}
|
||||
cherrypy.tree.mount(root=None, config=conf)
|
||||
|
||||
def test_Routes_Dispatch(self):
|
||||
"""Check that routes package based URI dispatching works correctly."""
|
||||
self.getPage('/hounslow')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('Welcome to Hounslow, pop. 10000')
|
||||
|
||||
self.getPage('/foo')
|
||||
self.assertStatus('404 Not Found')
|
||||
|
||||
self.getPage('/surbiton')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('Welcome to Surbiton, pop. 10000')
|
||||
|
||||
self.getPage('/surbiton', method='POST', body='pop=1327')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('OK')
|
||||
self.getPage('/surbiton')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Language', 'en-GB')
|
||||
self.assertBody('Welcome to Surbiton, pop. 1327')
|
||||
512
lib/cherrypy/test/test_session.py
Normal file
512
lib/cherrypy/test/test_session.py
Normal file
@@ -0,0 +1,512 @@
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import socket
|
||||
import importlib
|
||||
|
||||
from six.moves.http_client import HTTPConnection
|
||||
|
||||
import pytest
|
||||
from path import Path
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import (
|
||||
json_decode,
|
||||
HTTPSConnection,
|
||||
)
|
||||
from cherrypy.lib import sessions
|
||||
from cherrypy.lib import reprconf
|
||||
from cherrypy.lib.httputil import response_codes
|
||||
from cherrypy.test import helper
|
||||
|
||||
localDir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def http_methods_allowed(methods=['GET', 'HEAD']):
|
||||
method = cherrypy.request.method.upper()
|
||||
if method not in methods:
|
||||
cherrypy.response.headers['Allow'] = ', '.join(methods)
|
||||
raise cherrypy.HTTPError(405)
|
||||
|
||||
|
||||
cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
|
||||
|
||||
|
||||
def setup_server():
|
||||
|
||||
@cherrypy.config(**{
|
||||
'tools.sessions.on': True,
|
||||
'tools.sessions.storage_class': sessions.RamSession,
|
||||
'tools.sessions.storage_path': localDir,
|
||||
'tools.sessions.timeout': (1.0 / 60),
|
||||
'tools.sessions.clean_freq': (1.0 / 60),
|
||||
})
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def clear(self):
|
||||
cherrypy.session.cache.clear()
|
||||
|
||||
@cherrypy.expose
|
||||
def data(self):
|
||||
cherrypy.session['aha'] = 'foo'
|
||||
return repr(cherrypy.session._data)
|
||||
|
||||
@cherrypy.expose
|
||||
def testGen(self):
|
||||
counter = cherrypy.session.get('counter', 0) + 1
|
||||
cherrypy.session['counter'] = counter
|
||||
yield str(counter)
|
||||
|
||||
@cherrypy.expose
|
||||
def testStr(self):
|
||||
counter = cherrypy.session.get('counter', 0) + 1
|
||||
cherrypy.session['counter'] = counter
|
||||
return str(counter)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'tools.sessions.on': False})
|
||||
def set_session_cls(self, new_cls_name):
|
||||
new_cls = reprconf.attributes(new_cls_name)
|
||||
cfg = {'tools.sessions.storage_class': new_cls}
|
||||
self.__class__._cp_config.update(cfg)
|
||||
if hasattr(cherrypy, 'session'):
|
||||
del cherrypy.session
|
||||
if new_cls.clean_thread:
|
||||
new_cls.clean_thread.stop()
|
||||
new_cls.clean_thread.unsubscribe()
|
||||
del new_cls.clean_thread
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
sess = cherrypy.session
|
||||
c = sess.get('counter', 0) + 1
|
||||
time.sleep(0.01)
|
||||
sess['counter'] = c
|
||||
return str(c)
|
||||
|
||||
@cherrypy.expose
|
||||
def keyin(self, key):
|
||||
return str(key in cherrypy.session)
|
||||
|
||||
@cherrypy.expose
|
||||
def delete(self):
|
||||
cherrypy.session.delete()
|
||||
sessions.expire()
|
||||
return 'done'
|
||||
|
||||
@cherrypy.expose
|
||||
def delkey(self, key):
|
||||
del cherrypy.session[key]
|
||||
return 'OK'
|
||||
|
||||
@cherrypy.expose
|
||||
def redir_target(self):
|
||||
return self._cp_config['tools.sessions.storage_class'].__name__
|
||||
|
||||
@cherrypy.expose
|
||||
def iredir(self):
|
||||
raise cherrypy.InternalRedirect('/redir_target')
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.allow.on': True,
|
||||
'tools.allow.methods': ['GET'],
|
||||
})
|
||||
def restricted(self):
|
||||
return cherrypy.request.method
|
||||
|
||||
@cherrypy.expose
|
||||
def regen(self):
|
||||
cherrypy.tools.sessions.regenerate()
|
||||
return 'logged in'
|
||||
|
||||
@cherrypy.expose
|
||||
def length(self):
|
||||
return str(len(cherrypy.session))
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.sessions.path': '/session_cookie',
|
||||
'tools.sessions.name': 'temp',
|
||||
'tools.sessions.persistent': False,
|
||||
})
|
||||
def session_cookie(self):
|
||||
# Must load() to start the clean thread.
|
||||
cherrypy.session.load()
|
||||
return cherrypy.session.id
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
|
||||
class SessionTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def tearDown(self):
|
||||
# Clean up sessions.
|
||||
for fname in os.listdir(localDir):
|
||||
if fname.startswith(sessions.FileSession.SESSION_PREFIX):
|
||||
path = Path(localDir) / fname
|
||||
path.remove_p()
|
||||
|
||||
@pytest.mark.xfail(reason='#1534')
|
||||
def test_0_Session(self):
|
||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||
self.getPage('/clear')
|
||||
|
||||
# Test that a normal request gets the same id in the cookies.
|
||||
# Note: this wouldn't work if /data didn't load the session.
|
||||
self.getPage('/data')
|
||||
self.assertBody("{'aha': 'foo'}")
|
||||
c = self.cookies[0]
|
||||
self.getPage('/data', self.cookies)
|
||||
self.assertEqual(self.cookies[0], c)
|
||||
|
||||
self.getPage('/testStr')
|
||||
self.assertBody('1')
|
||||
cookie_parts = dict([p.strip().split('=')
|
||||
for p in self.cookies[0][1].split(';')])
|
||||
# Assert there is an 'expires' param
|
||||
self.assertEqual(set(cookie_parts.keys()),
|
||||
set(['session_id', 'expires', 'Path']))
|
||||
self.getPage('/testGen', self.cookies)
|
||||
self.assertBody('2')
|
||||
self.getPage('/testStr', self.cookies)
|
||||
self.assertBody('3')
|
||||
self.getPage('/data', self.cookies)
|
||||
self.assertDictEqual(json_decode(self.body),
|
||||
{'counter': 3, 'aha': 'foo'})
|
||||
self.getPage('/length', self.cookies)
|
||||
self.assertBody('2')
|
||||
self.getPage('/delkey?key=counter', self.cookies)
|
||||
self.assertStatus(200)
|
||||
|
||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
||||
self.getPage('/testStr')
|
||||
self.assertBody('1')
|
||||
self.getPage('/testGen', self.cookies)
|
||||
self.assertBody('2')
|
||||
self.getPage('/testStr', self.cookies)
|
||||
self.assertBody('3')
|
||||
self.getPage('/delkey?key=counter', self.cookies)
|
||||
self.assertStatus(200)
|
||||
|
||||
# Wait for the session.timeout (1 second)
|
||||
time.sleep(2)
|
||||
self.getPage('/')
|
||||
self.assertBody('1')
|
||||
self.getPage('/length', self.cookies)
|
||||
self.assertBody('1')
|
||||
|
||||
# Test session __contains__
|
||||
self.getPage('/keyin?key=counter', self.cookies)
|
||||
self.assertBody('True')
|
||||
cookieset1 = self.cookies
|
||||
|
||||
# Make a new session and test __len__ again
|
||||
self.getPage('/')
|
||||
self.getPage('/length', self.cookies)
|
||||
self.assertBody('2')
|
||||
|
||||
# Test session delete
|
||||
self.getPage('/delete', self.cookies)
|
||||
self.assertBody('done')
|
||||
self.getPage('/delete', cookieset1)
|
||||
self.assertBody('done')
|
||||
|
||||
def f():
|
||||
return [
|
||||
x
|
||||
for x in os.listdir(localDir)
|
||||
if x.startswith('session-')
|
||||
]
|
||||
self.assertEqual(f(), [])
|
||||
|
||||
# Wait for the cleanup thread to delete remaining session files
|
||||
self.getPage('/')
|
||||
self.assertNotEqual(f(), [])
|
||||
time.sleep(2)
|
||||
self.assertEqual(f(), [])
|
||||
|
||||
def test_1_Ram_Concurrency(self):
|
||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||
self._test_Concurrency()
|
||||
|
||||
@pytest.mark.xfail(reason='#1306')
|
||||
def test_2_File_Concurrency(self):
|
||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
||||
self._test_Concurrency()
|
||||
|
||||
def _test_Concurrency(self):
|
||||
client_thread_count = 5
|
||||
request_count = 30
|
||||
|
||||
# Get initial cookie
|
||||
self.getPage('/')
|
||||
self.assertBody('1')
|
||||
cookies = self.cookies
|
||||
|
||||
data_dict = {}
|
||||
errors = []
|
||||
|
||||
def request(index):
|
||||
if self.scheme == 'https':
|
||||
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
else:
|
||||
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
|
||||
for i in range(request_count):
|
||||
c.putrequest('GET', '/')
|
||||
for k, v in cookies:
|
||||
c.putheader(k, v)
|
||||
c.endheaders()
|
||||
response = c.getresponse()
|
||||
body = response.read()
|
||||
if response.status != 200 or not body.isdigit():
|
||||
errors.append((response.status, body))
|
||||
else:
|
||||
data_dict[index] = max(data_dict[index], int(body))
|
||||
# Uncomment the following line to prove threads overlap.
|
||||
# sys.stdout.write("%d " % index)
|
||||
|
||||
# Start <request_count> requests from each of
|
||||
# <client_thread_count> concurrent clients
|
||||
ts = []
|
||||
for c in range(client_thread_count):
|
||||
data_dict[c] = 0
|
||||
t = threading.Thread(target=request, args=(c,))
|
||||
ts.append(t)
|
||||
t.start()
|
||||
|
||||
for t in ts:
|
||||
t.join()
|
||||
|
||||
hitcount = max(data_dict.values())
|
||||
expected = 1 + (client_thread_count * request_count)
|
||||
|
||||
for e in errors:
|
||||
print(e)
|
||||
self.assertEqual(hitcount, expected)
|
||||
|
||||
def test_3_Redirect(self):
|
||||
# Start a new session
|
||||
self.getPage('/testStr')
|
||||
self.getPage('/iredir', self.cookies)
|
||||
self.assertBody('FileSession')
|
||||
|
||||
def test_4_File_deletion(self):
|
||||
# Start a new session
|
||||
self.getPage('/testStr')
|
||||
# Delete the session file manually and retry.
|
||||
id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||
path = os.path.join(localDir, 'session-' + id)
|
||||
os.unlink(path)
|
||||
self.getPage('/testStr', self.cookies)
|
||||
|
||||
def test_5_Error_paths(self):
|
||||
self.getPage('/unknown/page')
|
||||
self.assertErrorPage(404, "The path '/unknown/page' was not found.")
|
||||
|
||||
# Note: this path is *not* the same as above. The above
|
||||
# takes a normal route through the session code; this one
|
||||
# skips the session code's before_handler and only calls
|
||||
# before_finalize (save) and on_end (close). So the session
|
||||
# code has to survive calling save/close without init.
|
||||
self.getPage('/restricted', self.cookies, method='POST')
|
||||
self.assertErrorPage(405, response_codes[405][1])
|
||||
|
||||
def test_6_regenerate(self):
|
||||
self.getPage('/testStr')
|
||||
# grab the cookie ID
|
||||
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||
self.getPage('/regen')
|
||||
self.assertBody('logged in')
|
||||
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||
self.assertNotEqual(id1, id2)
|
||||
|
||||
self.getPage('/testStr')
|
||||
# grab the cookie ID
|
||||
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||
self.getPage('/testStr',
|
||||
headers=[
|
||||
('Cookie',
|
||||
'session_id=maliciousid; '
|
||||
'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
|
||||
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||
self.assertNotEqual(id1, id2)
|
||||
self.assertNotEqual(id2, 'maliciousid')
|
||||
|
||||
def test_7_session_cookies(self):
|
||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||
self.getPage('/clear')
|
||||
self.getPage('/session_cookie')
|
||||
# grab the cookie ID
|
||||
cookie_parts = dict([p.strip().split('=')
|
||||
for p in self.cookies[0][1].split(';')])
|
||||
# Assert there is no 'expires' param
|
||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
||||
id1 = cookie_parts['temp']
|
||||
self.assertEqual(list(sessions.RamSession.cache), [id1])
|
||||
|
||||
# Send another request in the same "browser session".
|
||||
self.getPage('/session_cookie', self.cookies)
|
||||
cookie_parts = dict([p.strip().split('=')
|
||||
for p in self.cookies[0][1].split(';')])
|
||||
# Assert there is no 'expires' param
|
||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
||||
self.assertBody(id1)
|
||||
self.assertEqual(list(sessions.RamSession.cache), [id1])
|
||||
|
||||
# Simulate a browser close by just not sending the cookies
|
||||
self.getPage('/session_cookie')
|
||||
# grab the cookie ID
|
||||
cookie_parts = dict([p.strip().split('=')
|
||||
for p in self.cookies[0][1].split(';')])
|
||||
# Assert there is no 'expires' param
|
||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
||||
# Assert a new id has been generated...
|
||||
id2 = cookie_parts['temp']
|
||||
self.assertNotEqual(id1, id2)
|
||||
self.assertEqual(set(sessions.RamSession.cache.keys()),
|
||||
set([id1, id2]))
|
||||
|
||||
# Wait for the session.timeout on both sessions
|
||||
time.sleep(2.5)
|
||||
cache = list(sessions.RamSession.cache)
|
||||
if cache:
|
||||
if cache == [id2]:
|
||||
self.fail('The second session did not time out.')
|
||||
else:
|
||||
self.fail('Unknown session id in cache: %r', cache)
|
||||
|
||||
def test_8_Ram_Cleanup(self):
|
||||
def lock():
|
||||
s1 = sessions.RamSession()
|
||||
s1.acquire_lock()
|
||||
time.sleep(1)
|
||||
s1.release_lock()
|
||||
|
||||
t = threading.Thread(target=lock)
|
||||
t.start()
|
||||
start = time.time()
|
||||
while not sessions.RamSession.locks and time.time() - start < 5:
|
||||
time.sleep(0.01)
|
||||
assert len(sessions.RamSession.locks) == 1, 'Lock not acquired'
|
||||
s2 = sessions.RamSession()
|
||||
s2.clean_up()
|
||||
msg = 'Clean up should not remove active lock'
|
||||
assert len(sessions.RamSession.locks) == 1, msg
|
||||
t.join()
|
||||
|
||||
|
||||
try:
|
||||
importlib.import_module('memcache')
|
||||
|
||||
host, port = '127.0.0.1', 11211
|
||||
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socktype, proto)
|
||||
# See http://groups.google.com/group/cherrypy-users/
|
||||
# browse_frm/thread/bbfe5eb39c904fe0
|
||||
s.settimeout(1.0)
|
||||
s.connect((host, port))
|
||||
s.close()
|
||||
except socket.error:
|
||||
if s:
|
||||
s.close()
|
||||
raise
|
||||
break
|
||||
except (ImportError, socket.error):
|
||||
class MemcachedSessionTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test(self):
|
||||
return self.skip('memcached not reachable ')
|
||||
else:
|
||||
class MemcachedSessionTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def test_0_Session(self):
|
||||
self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
|
||||
|
||||
self.getPage('/testStr')
|
||||
self.assertBody('1')
|
||||
self.getPage('/testGen', self.cookies)
|
||||
self.assertBody('2')
|
||||
self.getPage('/testStr', self.cookies)
|
||||
self.assertBody('3')
|
||||
self.getPage('/length', self.cookies)
|
||||
self.assertErrorPage(500)
|
||||
self.assertInBody('NotImplementedError')
|
||||
self.getPage('/delkey?key=counter', self.cookies)
|
||||
self.assertStatus(200)
|
||||
|
||||
# Wait for the session.timeout (1 second)
|
||||
time.sleep(1.25)
|
||||
self.getPage('/')
|
||||
self.assertBody('1')
|
||||
|
||||
# Test session __contains__
|
||||
self.getPage('/keyin?key=counter', self.cookies)
|
||||
self.assertBody('True')
|
||||
|
||||
# Test session delete
|
||||
self.getPage('/delete', self.cookies)
|
||||
self.assertBody('done')
|
||||
|
||||
def test_1_Concurrency(self):
|
||||
client_thread_count = 5
|
||||
request_count = 30
|
||||
|
||||
# Get initial cookie
|
||||
self.getPage('/')
|
||||
self.assertBody('1')
|
||||
cookies = self.cookies
|
||||
|
||||
data_dict = {}
|
||||
|
||||
def request(index):
|
||||
for i in range(request_count):
|
||||
self.getPage('/', cookies)
|
||||
# Uncomment the following line to prove threads overlap.
|
||||
# sys.stdout.write("%d " % index)
|
||||
if not self.body.isdigit():
|
||||
self.fail(self.body)
|
||||
data_dict[index] = int(self.body)
|
||||
|
||||
# Start <request_count> concurrent requests from
|
||||
# each of <client_thread_count> clients
|
||||
ts = []
|
||||
for c in range(client_thread_count):
|
||||
data_dict[c] = 0
|
||||
t = threading.Thread(target=request, args=(c,))
|
||||
ts.append(t)
|
||||
t.start()
|
||||
|
||||
for t in ts:
|
||||
t.join()
|
||||
|
||||
hitcount = max(data_dict.values())
|
||||
expected = 1 + (client_thread_count * request_count)
|
||||
self.assertEqual(hitcount, expected)
|
||||
|
||||
def test_3_Redirect(self):
|
||||
# Start a new session
|
||||
self.getPage('/testStr')
|
||||
self.getPage('/iredir', self.cookies)
|
||||
self.assertBody('memcached')
|
||||
|
||||
def test_5_Error_paths(self):
|
||||
self.getPage('/unknown/page')
|
||||
self.assertErrorPage(
|
||||
404, "The path '/unknown/page' was not found.")
|
||||
|
||||
# Note: this path is *not* the same as above. The above
|
||||
# takes a normal route through the session code; this one
|
||||
# skips the session code's before_handler and only calls
|
||||
# before_finalize (save) and on_end (close). So the session
|
||||
# code has to survive calling save/close without init.
|
||||
self.getPage('/restricted', self.cookies, method='POST')
|
||||
self.assertErrorPage(405, response_codes[405][1])
|
||||
61
lib/cherrypy/test/test_sessionauthenticate.py
Normal file
61
lib/cherrypy/test/test_sessionauthenticate.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class SessionAuthenticateTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
def check(username, password):
|
||||
# Dummy check_username_and_password function
|
||||
if username != 'test' or password != 'password':
|
||||
return 'Wrong login/password'
|
||||
|
||||
def augment_params():
|
||||
# A simple tool to add some things to request.params
|
||||
# This is to check to make sure that session_auth can handle
|
||||
# request params (ticket #780)
|
||||
cherrypy.request.params['test'] = 'test'
|
||||
|
||||
cherrypy.tools.augment_params = cherrypy.Tool(
|
||||
'before_handler', augment_params, None, priority=30)
|
||||
|
||||
class Test:
|
||||
|
||||
_cp_config = {
|
||||
'tools.sessions.on': True,
|
||||
'tools.session_auth.on': True,
|
||||
'tools.session_auth.check_username_and_password': check,
|
||||
'tools.augment_params.on': True,
|
||||
}
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, **kwargs):
|
||||
return 'Hi %s, you are logged in' % cherrypy.request.login
|
||||
|
||||
cherrypy.tree.mount(Test())
|
||||
|
||||
def testSessionAuthenticate(self):
|
||||
# request a page and check for login form
|
||||
self.getPage('/')
|
||||
self.assertInBody('<form method="post" action="do_login">')
|
||||
|
||||
# setup credentials
|
||||
login_body = 'username=test&password=password&from_page=/'
|
||||
|
||||
# attempt a login
|
||||
self.getPage('/do_login', method='POST', body=login_body)
|
||||
self.assertStatus((302, 303))
|
||||
|
||||
# get the page now that we are logged in
|
||||
self.getPage('/', self.cookies)
|
||||
self.assertBody('Hi test, you are logged in')
|
||||
|
||||
# do a logout
|
||||
self.getPage('/do_logout', self.cookies, method='POST')
|
||||
self.assertStatus((302, 303))
|
||||
|
||||
# verify we are logged out
|
||||
self.getPage('/', self.cookies)
|
||||
self.assertInBody('<form method="post" action="do_login">')
|
||||
473
lib/cherrypy/test/test_states.py
Normal file
473
lib/cherrypy/test/test_states.py
Normal file
@@ -0,0 +1,473 @@
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from six.moves.http_client import BadStatusLine
|
||||
|
||||
import pytest
|
||||
import portend
|
||||
|
||||
import cherrypy
|
||||
import cherrypy.process.servers
|
||||
from cherrypy.test import helper
|
||||
|
||||
engine = cherrypy.engine
|
||||
thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Dependency:
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
self.running = False
|
||||
self.startcount = 0
|
||||
self.gracecount = 0
|
||||
self.threads = {}
|
||||
|
||||
def subscribe(self):
|
||||
self.bus.subscribe('start', self.start)
|
||||
self.bus.subscribe('stop', self.stop)
|
||||
self.bus.subscribe('graceful', self.graceful)
|
||||
self.bus.subscribe('start_thread', self.startthread)
|
||||
self.bus.subscribe('stop_thread', self.stopthread)
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
self.startcount += 1
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
def graceful(self):
|
||||
self.gracecount += 1
|
||||
|
||||
def startthread(self, thread_id):
|
||||
self.threads[thread_id] = None
|
||||
|
||||
def stopthread(self, thread_id):
|
||||
del self.threads[thread_id]
|
||||
|
||||
|
||||
db_connection = Dependency(engine)
|
||||
|
||||
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hello World'
|
||||
|
||||
@cherrypy.expose
|
||||
def ctrlc(self):
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
@cherrypy.expose
|
||||
def graceful(self):
|
||||
engine.graceful()
|
||||
return 'app was (gracefully) restarted succesfully'
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({
|
||||
'environment': 'test_suite',
|
||||
})
|
||||
|
||||
db_connection.subscribe()
|
||||
|
||||
# ------------ Enough helpers. Time for real live test cases. ------------ #
|
||||
|
||||
|
||||
class ServerStateTests(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def setUp(self):
|
||||
cherrypy.server.socket_timeout = 0.1
|
||||
self.do_gc_test = False
|
||||
|
||||
def test_0_NormalStateFlow(self):
|
||||
engine.stop()
|
||||
# Our db_connection should not be running
|
||||
self.assertEqual(db_connection.running, False)
|
||||
self.assertEqual(db_connection.startcount, 1)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
# Test server start
|
||||
engine.start()
|
||||
self.assertEqual(engine.state, engine.states.STARTED)
|
||||
|
||||
host = cherrypy.server.socket_host
|
||||
port = cherrypy.server.socket_port
|
||||
portend.occupied(host, port, timeout=0.1)
|
||||
|
||||
# The db_connection should be running now
|
||||
self.assertEqual(db_connection.running, True)
|
||||
self.assertEqual(db_connection.startcount, 2)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
self.getPage('/')
|
||||
self.assertBody('Hello World')
|
||||
self.assertEqual(len(db_connection.threads), 1)
|
||||
|
||||
# Test engine stop. This will also stop the HTTP server.
|
||||
engine.stop()
|
||||
self.assertEqual(engine.state, engine.states.STOPPED)
|
||||
|
||||
# Verify that our custom stop function was called
|
||||
self.assertEqual(db_connection.running, False)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
# Block the main thread now and verify that exit() works.
|
||||
def exittest():
|
||||
self.getPage('/')
|
||||
self.assertBody('Hello World')
|
||||
engine.exit()
|
||||
cherrypy.server.start()
|
||||
engine.start_with_callback(exittest)
|
||||
engine.block()
|
||||
self.assertEqual(engine.state, engine.states.EXITING)
|
||||
|
||||
def test_1_Restart(self):
|
||||
cherrypy.server.start()
|
||||
engine.start()
|
||||
|
||||
# The db_connection should be running now
|
||||
self.assertEqual(db_connection.running, True)
|
||||
grace = db_connection.gracecount
|
||||
|
||||
self.getPage('/')
|
||||
self.assertBody('Hello World')
|
||||
self.assertEqual(len(db_connection.threads), 1)
|
||||
|
||||
# Test server restart from this thread
|
||||
engine.graceful()
|
||||
self.assertEqual(engine.state, engine.states.STARTED)
|
||||
self.getPage('/')
|
||||
self.assertBody('Hello World')
|
||||
self.assertEqual(db_connection.running, True)
|
||||
self.assertEqual(db_connection.gracecount, grace + 1)
|
||||
self.assertEqual(len(db_connection.threads), 1)
|
||||
|
||||
# Test server restart from inside a page handler
|
||||
self.getPage('/graceful')
|
||||
self.assertEqual(engine.state, engine.states.STARTED)
|
||||
self.assertBody('app was (gracefully) restarted succesfully')
|
||||
self.assertEqual(db_connection.running, True)
|
||||
self.assertEqual(db_connection.gracecount, grace + 2)
|
||||
# Since we are requesting synchronously, is only one thread used?
|
||||
# Note that the "/graceful" request has been flushed.
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
engine.stop()
|
||||
self.assertEqual(engine.state, engine.states.STOPPED)
|
||||
self.assertEqual(db_connection.running, False)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
def test_2_KeyboardInterrupt(self):
|
||||
# Raise a keyboard interrupt in the HTTP server's main thread.
|
||||
# We must start the server in this, the main thread
|
||||
engine.start()
|
||||
cherrypy.server.start()
|
||||
|
||||
self.persistent = True
|
||||
try:
|
||||
# Make the first request and assert there's no "Connection: close".
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('Hello World')
|
||||
self.assertNoHeader('Connection')
|
||||
|
||||
cherrypy.server.httpserver.interrupt = KeyboardInterrupt
|
||||
engine.block()
|
||||
|
||||
self.assertEqual(db_connection.running, False)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
self.assertEqual(engine.state, engine.states.EXITING)
|
||||
finally:
|
||||
self.persistent = False
|
||||
|
||||
# Raise a keyboard interrupt in a page handler; on multithreaded
|
||||
# servers, this should occur in one of the worker threads.
|
||||
# This should raise a BadStatusLine error, since the worker
|
||||
# thread will just die without writing a response.
|
||||
engine.start()
|
||||
cherrypy.server.start()
|
||||
# From python3.5 a new exception is retuned when the connection
|
||||
# ends abruptly:
|
||||
# http.client.RemoteDisconnected
|
||||
# RemoteDisconnected is a subclass of:
|
||||
# (ConnectionResetError, http.client.BadStatusLine)
|
||||
# and ConnectionResetError is an indirect subclass of:
|
||||
# OSError
|
||||
# From python 3.3 an up socket.error is an alias to OSError
|
||||
# following PEP-3151, therefore http.client.RemoteDisconnected
|
||||
# is considered a socket.error.
|
||||
#
|
||||
# raise_subcls specifies the classes that are not going
|
||||
# to be considered as a socket.error for the retries.
|
||||
# Given that RemoteDisconnected is part BadStatusLine
|
||||
# we can use the same call for all py3 versions without
|
||||
# sideffects. python < 3.5 will raise directly BadStatusLine
|
||||
# which is not a subclass for socket.error/OSError.
|
||||
try:
|
||||
self.getPage('/ctrlc', raise_subcls=BadStatusLine)
|
||||
except BadStatusLine:
|
||||
pass
|
||||
else:
|
||||
print(self.body)
|
||||
self.fail('AssertionError: BadStatusLine not raised')
|
||||
|
||||
engine.block()
|
||||
self.assertEqual(db_connection.running, False)
|
||||
self.assertEqual(len(db_connection.threads), 0)
|
||||
|
||||
@pytest.mark.xfail(
|
||||
'sys.platform == "Darwin" '
|
||||
'and sys.version_info > (3, 7) '
|
||||
'and os.environ["TRAVIS"]',
|
||||
reason='https://github.com/cherrypy/cherrypy/issues/1693',
|
||||
)
|
||||
def test_4_Autoreload(self):
|
||||
# If test_3 has not been executed, the server won't be stopped,
|
||||
# so we'll have to do it.
|
||||
if engine.state != engine.states.EXITING:
|
||||
engine.exit()
|
||||
|
||||
# Start the demo script in a new process
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
|
||||
p.write_conf(extra='test_case_name: "test_4_Autoreload"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
try:
|
||||
self.getPage('/start')
|
||||
start = float(self.body)
|
||||
|
||||
# Give the autoreloader time to cache the file time.
|
||||
time.sleep(2)
|
||||
|
||||
# Touch the file
|
||||
os.utime(os.path.join(thisdir, '_test_states_demo.py'), None)
|
||||
|
||||
# Give the autoreloader time to re-exec the process
|
||||
time.sleep(2)
|
||||
host = cherrypy.server.socket_host
|
||||
port = cherrypy.server.socket_port
|
||||
portend.occupied(host, port, timeout=5)
|
||||
|
||||
self.getPage('/start')
|
||||
if not (float(self.body) > start):
|
||||
raise AssertionError('start time %s not greater than %s' %
|
||||
(float(self.body), start))
|
||||
finally:
|
||||
# Shut down the spawned process
|
||||
self.getPage('/exit')
|
||||
p.join()
|
||||
|
||||
def test_5_Start_Error(self):
|
||||
# If test_3 has not been executed, the server won't be stopped,
|
||||
# so we'll have to do it.
|
||||
if engine.state != engine.states.EXITING:
|
||||
engine.exit()
|
||||
|
||||
# If a process errors during start, it should stop the engine
|
||||
# and exit with a non-zero exit code.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
|
||||
wait=True)
|
||||
p.write_conf(
|
||||
extra="""starterror: True
|
||||
test_case_name: "test_5_Start_Error"
|
||||
"""
|
||||
)
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
if p.exit_code == 0:
|
||||
self.fail('Process failed to return nonzero exit code.')
|
||||
|
||||
|
||||
class PluginTests(helper.CPWebCase):
|
||||
|
||||
def test_daemonize(self):
|
||||
if os.name not in ['posix']:
|
||||
return self.skip('skipped (not on posix) ')
|
||||
self.HOST = '127.0.0.1'
|
||||
self.PORT = 8081
|
||||
# Spawn the process and wait, when this returns, the original process
|
||||
# is finished. If it daemonized properly, we should still be able
|
||||
# to access pages.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
|
||||
wait=True, daemonize=True,
|
||||
socket_host='127.0.0.1',
|
||||
socket_port=8081)
|
||||
p.write_conf(
|
||||
extra='test_case_name: "test_daemonize"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
try:
|
||||
# Just get the pid of the daemonization process.
|
||||
self.getPage('/pid')
|
||||
self.assertStatus(200)
|
||||
page_pid = int(self.body)
|
||||
self.assertEqual(page_pid, p.get_pid())
|
||||
finally:
|
||||
# Shut down the spawned process
|
||||
self.getPage('/exit')
|
||||
p.join()
|
||||
|
||||
# Wait until here to test the exit code because we want to ensure
|
||||
# that we wait for the daemon to finish running before we fail.
|
||||
if p.exit_code != 0:
|
||||
self.fail('Daemonized parent process failed to exit cleanly.')
|
||||
|
||||
|
||||
class SignalHandlingTests(helper.CPWebCase):
|
||||
|
||||
def test_SIGHUP_tty(self):
|
||||
# When not daemonized, SIGHUP should shut down the server.
|
||||
try:
|
||||
from signal import SIGHUP
|
||||
except ImportError:
|
||||
return self.skip('skipped (no SIGHUP) ')
|
||||
|
||||
# Spawn the process.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
|
||||
p.write_conf(
|
||||
extra='test_case_name: "test_SIGHUP_tty"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
# Send a SIGHUP
|
||||
os.kill(p.get_pid(), SIGHUP)
|
||||
# This might hang if things aren't working right, but meh.
|
||||
p.join()
|
||||
|
||||
def test_SIGHUP_daemonized(self):
|
||||
# When daemonized, SIGHUP should restart the server.
|
||||
try:
|
||||
from signal import SIGHUP
|
||||
except ImportError:
|
||||
return self.skip('skipped (no SIGHUP) ')
|
||||
|
||||
if os.name not in ['posix']:
|
||||
return self.skip('skipped (not on posix) ')
|
||||
|
||||
# Spawn the process and wait, when this returns, the original process
|
||||
# is finished. If it daemonized properly, we should still be able
|
||||
# to access pages.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
|
||||
wait=True, daemonize=True)
|
||||
p.write_conf(
|
||||
extra='test_case_name: "test_SIGHUP_daemonized"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
|
||||
pid = p.get_pid()
|
||||
try:
|
||||
# Send a SIGHUP
|
||||
os.kill(pid, SIGHUP)
|
||||
# Give the server some time to restart
|
||||
time.sleep(2)
|
||||
self.getPage('/pid')
|
||||
self.assertStatus(200)
|
||||
new_pid = int(self.body)
|
||||
self.assertNotEqual(new_pid, pid)
|
||||
finally:
|
||||
# Shut down the spawned process
|
||||
self.getPage('/exit')
|
||||
p.join()
|
||||
|
||||
def _require_signal_and_kill(self, signal_name):
|
||||
if not hasattr(signal, signal_name):
|
||||
self.skip('skipped (no %(signal_name)s)' % vars())
|
||||
|
||||
if not hasattr(os, 'kill'):
|
||||
self.skip('skipped (no os.kill)')
|
||||
|
||||
def test_SIGTERM(self):
|
||||
'SIGTERM should shut down the server whether daemonized or not.'
|
||||
self._require_signal_and_kill('SIGTERM')
|
||||
|
||||
# Spawn a normal, undaemonized process.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
|
||||
p.write_conf(
|
||||
extra='test_case_name: "test_SIGTERM"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
# Send a SIGTERM
|
||||
os.kill(p.get_pid(), signal.SIGTERM)
|
||||
# This might hang if things aren't working right, but meh.
|
||||
p.join()
|
||||
|
||||
if os.name in ['posix']:
|
||||
# Spawn a daemonized process and test again.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
|
||||
wait=True, daemonize=True)
|
||||
p.write_conf(
|
||||
extra='test_case_name: "test_SIGTERM_2"')
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
# Send a SIGTERM
|
||||
os.kill(p.get_pid(), signal.SIGTERM)
|
||||
# This might hang if things aren't working right, but meh.
|
||||
p.join()
|
||||
|
||||
def test_signal_handler_unsubscribe(self):
|
||||
self._require_signal_and_kill('SIGTERM')
|
||||
|
||||
# Although Windows has `os.kill` and SIGTERM is defined, the
|
||||
# platform does not implement signals and sending SIGTERM
|
||||
# will result in a forced termination of the process.
|
||||
# Therefore, this test is not suitable for Windows.
|
||||
if os.name == 'nt':
|
||||
self.skip('SIGTERM not available')
|
||||
|
||||
# Spawn a normal, undaemonized process.
|
||||
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
|
||||
p.write_conf(
|
||||
extra="""unsubsig: True
|
||||
test_case_name: "test_signal_handler_unsubscribe"
|
||||
""")
|
||||
p.start(imports='cherrypy.test._test_states_demo')
|
||||
# Ask the process to quit
|
||||
os.kill(p.get_pid(), signal.SIGTERM)
|
||||
# This might hang if things aren't working right, but meh.
|
||||
p.join()
|
||||
|
||||
# Assert the old handler ran.
|
||||
log_lines = list(open(p.error_log, 'rb'))
|
||||
assert any(
|
||||
line.endswith(b'I am an old SIGTERM handler.\n')
|
||||
for line in log_lines
|
||||
)
|
||||
|
||||
|
||||
class WaitTests(unittest.TestCase):
|
||||
|
||||
def test_safe_wait_INADDR_ANY(self):
|
||||
"""
|
||||
Wait on INADDR_ANY should not raise IOError
|
||||
|
||||
In cases where the loopback interface does not exist, CherryPy cannot
|
||||
effectively determine if a port binding to INADDR_ANY was effected.
|
||||
In this situation, CherryPy should assume that it failed to detect
|
||||
the binding (not that the binding failed) and only warn that it could
|
||||
not verify it.
|
||||
"""
|
||||
# At such a time that CherryPy can reliably determine one or more
|
||||
# viable IP addresses of the host, this test may be removed.
|
||||
|
||||
# Simulate the behavior we observe when no loopback interface is
|
||||
# present by: finding a port that's not occupied, then wait on it.
|
||||
|
||||
free_port = portend.find_available_local_port()
|
||||
|
||||
servers = cherrypy.process.servers
|
||||
|
||||
inaddr_any = '0.0.0.0'
|
||||
|
||||
# Wait on the free port that's unbound
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
with servers._safe_wait(inaddr_any, free_port):
|
||||
portend.occupied(inaddr_any, free_port, timeout=1)
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(isinstance(w[0], warnings.WarningMessage))
|
||||
self.assertTrue(
|
||||
'Unable to verify that the server is bound on ' in str(w[0]))
|
||||
|
||||
# The wait should still raise an IO error if INADDR_ANY was
|
||||
# not supplied.
|
||||
with pytest.raises(IOError):
|
||||
with servers._safe_wait('127.0.0.1', free_port):
|
||||
portend.occupied('127.0.0.1', free_port, timeout=1)
|
||||
438
lib/cherrypy/test/test_static.py
Normal file
438
lib/cherrypy/test/test_static.py
Normal file
@@ -0,0 +1,438 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
from six import text_type as str
|
||||
from six.moves import urllib
|
||||
from six.moves.http_client import HTTPConnection
|
||||
|
||||
import pytest
|
||||
import py.path
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import static
|
||||
from cherrypy._cpcompat import HTTPSConnection, ntou, tonative
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unicode_filesystem(tmpdir):
|
||||
_check_unicode_filesystem(tmpdir)
|
||||
|
||||
|
||||
def _check_unicode_filesystem(tmpdir):
|
||||
filename = tmpdir / ntou('☃', 'utf-8')
|
||||
tmpl = 'File system encoding ({encoding}) cannot support unicode filenames'
|
||||
msg = tmpl.format(encoding=sys.getfilesystemencoding())
|
||||
try:
|
||||
io.open(str(filename), 'w').close()
|
||||
except UnicodeEncodeError:
|
||||
pytest.skip(msg)
|
||||
|
||||
|
||||
def ensure_unicode_filesystem():
|
||||
"""
|
||||
TODO: replace with simply pytest fixtures once webtest.TestCase
|
||||
no longer implies unittest.
|
||||
"""
|
||||
tmpdir = py.path.local(tempfile.mkdtemp())
|
||||
try:
|
||||
_check_unicode_filesystem(tmpdir)
|
||||
finally:
|
||||
tmpdir.remove()
|
||||
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
|
||||
bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log')
|
||||
|
||||
# The file size needs to be big enough such that half the size of it
|
||||
# won't be socket-buffered (or server-buffered) all in one go. See
|
||||
# test_file_stream.
|
||||
MB = 2 ** 20
|
||||
BIGFILE_SIZE = 32 * MB
|
||||
|
||||
|
||||
class StaticTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
if not os.path.exists(has_space_filepath):
|
||||
with open(has_space_filepath, 'wb') as f:
|
||||
f.write(b'Hello, world\r\n')
|
||||
needs_bigfile = (
|
||||
not os.path.exists(bigfile_filepath) or
|
||||
os.path.getsize(bigfile_filepath) != BIGFILE_SIZE
|
||||
)
|
||||
if needs_bigfile:
|
||||
with open(bigfile_filepath, 'wb') as f:
|
||||
f.write(b'x' * BIGFILE_SIZE)
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
def bigfile(self):
|
||||
self.f = static.serve_file(bigfile_filepath)
|
||||
return self.f
|
||||
|
||||
@cherrypy.expose
|
||||
def tell(self):
|
||||
if self.f.input.closed:
|
||||
return ''
|
||||
return repr(self.f.input.tell()).rstrip('L')
|
||||
|
||||
@cherrypy.expose
|
||||
def fileobj(self):
|
||||
f = open(os.path.join(curdir, 'style.css'), 'rb')
|
||||
return static.serve_fileobj(f, content_type='text/css')
|
||||
|
||||
@cherrypy.expose
|
||||
def bytesio(self):
|
||||
f = io.BytesIO(b'Fee\nfie\nfo\nfum')
|
||||
return static.serve_fileobj(f, content_type='text/plain')
|
||||
|
||||
class Static:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'You want the Baron? You can have the Baron!'
|
||||
|
||||
@cherrypy.expose
|
||||
def dynamic(self):
|
||||
return 'This is a DYNAMIC page'
|
||||
|
||||
root = Root()
|
||||
root.static = Static()
|
||||
|
||||
rootconf = {
|
||||
'/static': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.root': curdir,
|
||||
},
|
||||
'/static-long': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': r'\\?\%s' % curdir,
|
||||
},
|
||||
'/style.css': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
|
||||
},
|
||||
'/docroot': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.root': curdir,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.index': 'index.html',
|
||||
},
|
||||
'/error': {
|
||||
'tools.staticdir.on': True,
|
||||
'request.show_tracebacks': True,
|
||||
},
|
||||
'/404test': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.root': curdir,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'error_page.404': error_page_404,
|
||||
}
|
||||
}
|
||||
rootApp = cherrypy.Application(root)
|
||||
rootApp.merge(rootconf)
|
||||
|
||||
test_app_conf = {
|
||||
'/test': {
|
||||
'tools.staticdir.index': 'index.html',
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.root': curdir,
|
||||
'tools.staticdir.dir': 'static',
|
||||
},
|
||||
}
|
||||
testApp = cherrypy.Application(Static())
|
||||
testApp.merge(test_app_conf)
|
||||
|
||||
vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
|
||||
cherrypy.tree.graft(vhost)
|
||||
|
||||
@staticmethod
|
||||
def teardown_server():
|
||||
for f in (has_space_filepath, bigfile_filepath):
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
os.unlink(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_static(self):
|
||||
self.getPage('/static/index.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
|
||||
# Using a staticdir.root value in a subdir...
|
||||
self.getPage('/docroot/index.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
|
||||
# Check a filename with spaces in it
|
||||
self.getPage('/static/has%20space.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
|
||||
self.getPage('/style.css')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/css')
|
||||
# Note: The body should be exactly 'Dummy stylesheet\n', but
|
||||
# unfortunately some tools such as WinZip sometimes turn \n
|
||||
# into \r\n on Windows when extracting the CherryPy tarball so
|
||||
# we just check the content
|
||||
self.assertMatchesBody('^Dummy stylesheet')
|
||||
|
||||
@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
|
||||
def test_static_longpath(self):
|
||||
"""Test serving of a file in subdir of a Windows long-path
|
||||
staticdir."""
|
||||
self.getPage('/static-long/static/index.html')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
|
||||
def test_fallthrough(self):
|
||||
# Test that NotFound will then try dynamic handlers (see [878]).
|
||||
self.getPage('/static/dynamic')
|
||||
self.assertBody('This is a DYNAMIC page')
|
||||
|
||||
# Check a directory via fall-through to dynamic handler.
|
||||
self.getPage('/static/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
|
||||
self.assertBody('You want the Baron? You can have the Baron!')
|
||||
|
||||
def test_index(self):
|
||||
# Check a directory via "staticdir.index".
|
||||
self.getPage('/docroot/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/html')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
# The same page should be returned even if redirected.
|
||||
self.getPage('/docroot')
|
||||
self.assertStatus(301)
|
||||
self.assertHeader('Location', '%s/docroot/' % self.base())
|
||||
self.assertMatchesBody(
|
||||
"This resource .* <a href=(['\"])%s/docroot/\\1>"
|
||||
'%s/docroot/</a>.'
|
||||
% (self.base(), self.base())
|
||||
)
|
||||
|
||||
def test_config_errors(self):
|
||||
# Check that we get an error if no .file or .dir
|
||||
self.getPage('/error/thing.html')
|
||||
self.assertErrorPage(500)
|
||||
if sys.version_info >= (3, 3):
|
||||
errmsg = (
|
||||
r'TypeError: staticdir\(\) missing 2 '
|
||||
'required positional arguments'
|
||||
)
|
||||
else:
|
||||
errmsg = (
|
||||
r'TypeError: staticdir\(\) takes at least 2 '
|
||||
r'(positional )?arguments \(0 given\)'
|
||||
)
|
||||
self.assertMatchesBody(errmsg.encode('ascii'))
|
||||
|
||||
def test_security(self):
|
||||
# Test up-level security
|
||||
self.getPage('/static/../../test/style.css')
|
||||
self.assertStatus((400, 403))
|
||||
|
||||
def test_modif(self):
|
||||
# Test modified-since on a reasonably-large file
|
||||
self.getPage('/static/dirback.jpg')
|
||||
self.assertStatus('200 OK')
|
||||
lastmod = ''
|
||||
for k, v in self.headers:
|
||||
if k == 'Last-Modified':
|
||||
lastmod = v
|
||||
ims = ('If-Modified-Since', lastmod)
|
||||
self.getPage('/static/dirback.jpg', headers=[ims])
|
||||
self.assertStatus(304)
|
||||
self.assertNoHeader('Content-Type')
|
||||
self.assertNoHeader('Content-Length')
|
||||
self.assertNoHeader('Content-Disposition')
|
||||
self.assertBody('')
|
||||
|
||||
def test_755_vhost(self):
|
||||
self.getPage('/test/', [('Host', 'virt.net')])
|
||||
self.assertStatus(200)
|
||||
self.getPage('/test', [('Host', 'virt.net')])
|
||||
self.assertStatus(301)
|
||||
self.assertHeader('Location', self.scheme + '://virt.net/test/')
|
||||
|
||||
def test_serve_fileobj(self):
|
||||
self.getPage('/fileobj')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/css;charset=utf-8')
|
||||
self.assertMatchesBody('^Dummy stylesheet')
|
||||
|
||||
def test_serve_bytesio(self):
|
||||
self.getPage('/bytesio')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
|
||||
self.assertHeader('Content-Length', 14)
|
||||
self.assertMatchesBody('Fee\nfie\nfo\nfum')
|
||||
|
||||
@pytest.mark.xfail(reason='#1475')
|
||||
def test_file_stream(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Make an initial request
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/bigfile', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
body = b''
|
||||
remaining = BIGFILE_SIZE
|
||||
while remaining > 0:
|
||||
data = response.fp.read(65536)
|
||||
if not data:
|
||||
break
|
||||
body += data
|
||||
remaining -= len(data)
|
||||
|
||||
if self.scheme == 'https':
|
||||
newconn = HTTPSConnection
|
||||
else:
|
||||
newconn = HTTPConnection
|
||||
s, h, b = helper.webtest.openURL(
|
||||
b'/tell', headers=[], host=self.HOST, port=self.PORT,
|
||||
http_conn=newconn)
|
||||
if not b:
|
||||
# The file was closed on the server.
|
||||
tell_position = BIGFILE_SIZE
|
||||
else:
|
||||
tell_position = int(b)
|
||||
|
||||
read_so_far = len(body)
|
||||
|
||||
# It is difficult for us to force the server to only read
|
||||
# the bytes that we ask for - there are going to be buffers
|
||||
# inbetween.
|
||||
#
|
||||
# CherryPy will attempt to write as much data as it can to
|
||||
# the socket, and we don't have a way to determine what that
|
||||
# size will be. So we make the following assumption - by
|
||||
# the time we have read in the entire file on the server,
|
||||
# we will have at least received half of it. If this is not
|
||||
# the case, then this is an indicator that either:
|
||||
# - machines that are running this test are using buffer
|
||||
# sizes greater than half of BIGFILE_SIZE; or
|
||||
# - streaming is broken.
|
||||
#
|
||||
# At the time of writing, we seem to have encountered
|
||||
# buffer sizes bigger than 512K, so we've increased
|
||||
# BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB.
|
||||
# This test is going to keep failing according to the
|
||||
# improvements in hardware and OS buffers.
|
||||
if tell_position >= BIGFILE_SIZE:
|
||||
if read_so_far < (BIGFILE_SIZE / 2):
|
||||
self.fail(
|
||||
'The file should have advanced to position %r, but '
|
||||
'has already advanced to the end of the file. It '
|
||||
'may not be streamed as intended, or at the wrong '
|
||||
'chunk size (64k)' % read_so_far)
|
||||
elif tell_position < read_so_far:
|
||||
self.fail(
|
||||
'The file should have advanced to position %r, but has '
|
||||
'only advanced to position %r. It may not be streamed '
|
||||
'as intended, or at the wrong chunk size (64k)' %
|
||||
(read_so_far, tell_position))
|
||||
|
||||
if body != b'x' * BIGFILE_SIZE:
|
||||
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
|
||||
(BIGFILE_SIZE, body[:50], len(body)))
|
||||
conn.close()
|
||||
|
||||
def test_file_stream_deadlock(self):
|
||||
if cherrypy.server.protocol_version != 'HTTP/1.1':
|
||||
return self.skip()
|
||||
|
||||
self.PROTOCOL = 'HTTP/1.1'
|
||||
|
||||
# Make an initial request but abort early.
|
||||
self.persistent = True
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/bigfile', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
self.assertEqual(response.status, 200)
|
||||
body = response.fp.read(65536)
|
||||
if body != b'x' * len(body):
|
||||
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
|
||||
(65536, body[:50], len(body)))
|
||||
response.close()
|
||||
conn.close()
|
||||
|
||||
# Make a second request, which should fetch the whole file.
|
||||
self.persistent = False
|
||||
self.getPage('/bigfile')
|
||||
if self.body != b'x' * BIGFILE_SIZE:
|
||||
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
|
||||
(BIGFILE_SIZE, self.body[:50], len(body)))
|
||||
|
||||
def test_error_page_with_serve_file(self):
|
||||
self.getPage('/404test/yunyeen')
|
||||
self.assertStatus(404)
|
||||
self.assertInBody("I couldn't find that thing")
|
||||
|
||||
def test_null_bytes(self):
|
||||
self.getPage('/static/\x00')
|
||||
self.assertStatus('404 Not Found')
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def unicode_file():
|
||||
filename = ntou('Слава Україні.html', 'utf-8')
|
||||
filepath = os.path.join(curdir, 'static', filename)
|
||||
with io.open(filepath, 'w', encoding='utf-8') as strm:
|
||||
strm.write(ntou('Героям Слава!', 'utf-8'))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.remove(filepath)
|
||||
|
||||
py27_on_windows = (
|
||||
platform.system() == 'Windows' and
|
||||
sys.version_info < (3,)
|
||||
)
|
||||
@pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301
|
||||
def test_unicode(self):
|
||||
ensure_unicode_filesystem()
|
||||
with self.unicode_file():
|
||||
url = ntou('/static/Слава Україні.html', 'utf-8')
|
||||
# quote function requires str
|
||||
url = tonative(url, 'utf-8')
|
||||
url = urllib.parse.quote(url)
|
||||
self.getPage(url)
|
||||
|
||||
expected = ntou('Героям Слава!', 'utf-8')
|
||||
self.assertInBody(expected)
|
||||
|
||||
|
||||
def error_page_404(status, message, traceback, version):
|
||||
path = os.path.join(curdir, 'static', '404.html')
|
||||
return static.serve_file(path, content_type='text/html')
|
||||
468
lib/cherrypy/test/test_tools.py
Normal file
468
lib/cherrypy/test/test_tools.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""Test the various means of instantiating and invoking tools."""
|
||||
|
||||
import gzip
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
import unittest
|
||||
import operator
|
||||
|
||||
import six
|
||||
from six.moves import range, map
|
||||
from six.moves.http_client import IncompleteRead
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import tools
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy.test import helper, _test_decorators
|
||||
|
||||
|
||||
timeout = 0.2
|
||||
europoundUnicode = ntou('\x80\xa3')
|
||||
|
||||
|
||||
# Client-side code #
|
||||
|
||||
|
||||
class ToolTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
# Put check_access in a custom toolbox with its own namespace
|
||||
myauthtools = cherrypy._cptools.Toolbox('myauth')
|
||||
|
||||
def check_access(default=False):
|
||||
if not getattr(cherrypy.request, 'userid', default):
|
||||
raise cherrypy.HTTPError(401)
|
||||
myauthtools.check_access = cherrypy.Tool(
|
||||
'before_request_body', check_access)
|
||||
|
||||
def numerify():
|
||||
def number_it(body):
|
||||
for chunk in body:
|
||||
for k, v in cherrypy.request.numerify_map:
|
||||
chunk = chunk.replace(k, v)
|
||||
yield chunk
|
||||
cherrypy.response.body = number_it(cherrypy.response.body)
|
||||
|
||||
class NumTool(cherrypy.Tool):
|
||||
|
||||
def _setup(self):
|
||||
def makemap():
|
||||
m = self._merged_args().get('map', {})
|
||||
cherrypy.request.numerify_map = list(six.iteritems(m))
|
||||
cherrypy.request.hooks.attach('on_start_resource', makemap)
|
||||
|
||||
def critical():
|
||||
cherrypy.request.error_response = cherrypy.HTTPError(
|
||||
502).set_response
|
||||
critical.failsafe = True
|
||||
|
||||
cherrypy.request.hooks.attach('on_start_resource', critical)
|
||||
cherrypy.request.hooks.attach(self._point, self.callable)
|
||||
|
||||
tools.numerify = NumTool('before_finalize', numerify)
|
||||
|
||||
# It's not mandatory to inherit from cherrypy.Tool.
|
||||
class NadsatTool:
|
||||
|
||||
def __init__(self):
|
||||
self.ended = {}
|
||||
self._name = 'nadsat'
|
||||
|
||||
def nadsat(self):
|
||||
def nadsat_it_up(body):
|
||||
for chunk in body:
|
||||
chunk = chunk.replace(b'good', b'horrorshow')
|
||||
chunk = chunk.replace(b'piece', b'lomtick')
|
||||
yield chunk
|
||||
cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
|
||||
nadsat.priority = 0
|
||||
|
||||
def cleanup(self):
|
||||
# This runs after the request has been completely written out.
|
||||
cherrypy.response.body = [b'razdrez']
|
||||
id = cherrypy.request.params.get('id')
|
||||
if id:
|
||||
self.ended[id] = True
|
||||
cleanup.failsafe = True
|
||||
|
||||
def _setup(self):
|
||||
cherrypy.request.hooks.attach('before_finalize', self.nadsat)
|
||||
cherrypy.request.hooks.attach('on_end_request', self.cleanup)
|
||||
tools.nadsat = NadsatTool()
|
||||
|
||||
def pipe_body():
|
||||
cherrypy.request.process_request_body = False
|
||||
clen = int(cherrypy.request.headers['Content-Length'])
|
||||
cherrypy.request.body = cherrypy.request.rfile.read(clen)
|
||||
|
||||
# Assert that we can use a callable object instead of a function.
|
||||
class Rotator(object):
|
||||
|
||||
def __call__(self, scale):
|
||||
r = cherrypy.response
|
||||
r.collapse_body()
|
||||
if six.PY3:
|
||||
r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
|
||||
else:
|
||||
r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
|
||||
cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
|
||||
|
||||
def stream_handler(next_handler, *args, **kwargs):
|
||||
actual = cherrypy.request.config.get('tools.streamer.arg')
|
||||
assert actual == 'arg value'
|
||||
cherrypy.response.output = o = io.BytesIO()
|
||||
try:
|
||||
next_handler(*args, **kwargs)
|
||||
# Ignore the response and return our accumulated output
|
||||
# instead.
|
||||
return o.getvalue()
|
||||
finally:
|
||||
o.close()
|
||||
cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
|
||||
stream_handler)
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Howdy earth!'
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{
|
||||
'tools.streamer.on': True,
|
||||
'tools.streamer.arg': 'arg value',
|
||||
})
|
||||
def tarfile(self):
|
||||
actual = cherrypy.request.config.get('tools.streamer.arg')
|
||||
assert actual == 'arg value'
|
||||
cherrypy.response.output.write(b'I am ')
|
||||
cherrypy.response.output.write(b'a tarfile')
|
||||
|
||||
@cherrypy.expose
|
||||
def euro(self):
|
||||
hooks = list(cherrypy.request.hooks['before_finalize'])
|
||||
hooks.sort()
|
||||
cbnames = [x.callback.__name__ for x in hooks]
|
||||
assert cbnames == ['gzip'], cbnames
|
||||
priorities = [x.priority for x in hooks]
|
||||
assert priorities == [80], priorities
|
||||
yield ntou('Hello,')
|
||||
yield ntou('world')
|
||||
yield europoundUnicode
|
||||
|
||||
# Bare hooks
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'hooks.before_request_body': pipe_body})
|
||||
def pipe(self):
|
||||
return cherrypy.request.body
|
||||
|
||||
# Multiple decorators; include kwargs just for fun.
|
||||
# Note that rotator must run before gzip.
|
||||
@cherrypy.expose
|
||||
def decorated_euro(self, *vpath):
|
||||
yield ntou('Hello,')
|
||||
yield ntou('world')
|
||||
yield europoundUnicode
|
||||
decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
|
||||
decorated_euro = tools.rotator(scale=3)(decorated_euro)
|
||||
|
||||
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 six.itervalues(dct):
|
||||
if isinstance(value, types.FunctionType):
|
||||
cherrypy.expose(value)
|
||||
setattr(root, name.lower(), cls())
|
||||
Test = TestType('Test', (object,), {})
|
||||
|
||||
# METHOD ONE:
|
||||
# Declare Tools in _cp_config
|
||||
@cherrypy.config(**{'tools.nadsat.on': True})
|
||||
class Demo(Test):
|
||||
|
||||
def index(self, id=None):
|
||||
return 'A good piece of cherry pie'
|
||||
|
||||
def ended(self, id):
|
||||
return repr(tools.nadsat.ended[id])
|
||||
|
||||
def err(self, id=None):
|
||||
raise ValueError()
|
||||
|
||||
def errinstream(self, id=None):
|
||||
yield 'nonconfidential'
|
||||
raise ValueError()
|
||||
yield 'confidential'
|
||||
|
||||
# METHOD TWO: decorator using Tool()
|
||||
# We support Python 2.3, but the @-deco syntax would look like
|
||||
# this:
|
||||
# @tools.check_access()
|
||||
def restricted(self):
|
||||
return 'Welcome!'
|
||||
restricted = myauthtools.check_access()(restricted)
|
||||
userid = restricted
|
||||
|
||||
def err_in_onstart(self):
|
||||
return 'success!'
|
||||
|
||||
@cherrypy.config(**{'response.stream': True})
|
||||
def stream(self, id=None):
|
||||
for x in range(100000000):
|
||||
yield str(x)
|
||||
|
||||
conf = {
|
||||
# METHOD THREE:
|
||||
# Declare Tools in detached config
|
||||
'/demo': {
|
||||
'tools.numerify.on': True,
|
||||
'tools.numerify.map': {b'pie': b'3.14159'},
|
||||
},
|
||||
'/demo/restricted': {
|
||||
'request.show_tracebacks': False,
|
||||
},
|
||||
'/demo/userid': {
|
||||
'request.show_tracebacks': False,
|
||||
'myauth.check_access.default': True,
|
||||
},
|
||||
'/demo/errinstream': {
|
||||
'response.stream': True,
|
||||
},
|
||||
'/demo/err_in_onstart': {
|
||||
# Because this isn't a dict, on_start_resource will error.
|
||||
'tools.numerify.map': 'pie->3.14159'
|
||||
},
|
||||
# Combined tools
|
||||
'/euro': {
|
||||
'tools.gzip.on': True,
|
||||
'tools.encode.on': True,
|
||||
},
|
||||
# Priority specified in config
|
||||
'/decorated_euro/subpath': {
|
||||
'tools.gzip.priority': 10,
|
||||
},
|
||||
# Handler wrappers
|
||||
'/tarfile': {'tools.streamer.on': True}
|
||||
}
|
||||
app = cherrypy.tree.mount(root, config=conf)
|
||||
app.request_class.namespaces['myauth'] = myauthtools
|
||||
|
||||
root.tooldecs = _test_decorators.ToolExamples()
|
||||
|
||||
def testHookErrors(self):
|
||||
self.getPage('/demo/?id=1')
|
||||
# If body is "razdrez", then on_end_request is being called too early.
|
||||
self.assertBody('A horrorshow lomtick of cherry 3.14159')
|
||||
# If this fails, then on_end_request isn't being called at all.
|
||||
time.sleep(0.1)
|
||||
self.getPage('/demo/ended/1')
|
||||
self.assertBody('True')
|
||||
|
||||
valerr = '\n raise ValueError()\nValueError'
|
||||
self.getPage('/demo/err?id=3')
|
||||
# If body is "razdrez", then on_end_request is being called too early.
|
||||
self.assertErrorPage(502, pattern=valerr)
|
||||
# If this fails, then on_end_request isn't being called at all.
|
||||
time.sleep(0.1)
|
||||
self.getPage('/demo/ended/3')
|
||||
self.assertBody('True')
|
||||
|
||||
# If body is "razdrez", then on_end_request is being called too early.
|
||||
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
|
||||
getattr(cherrypy.server, 'using_apache', False)):
|
||||
self.getPage('/demo/errinstream?id=5')
|
||||
# Because this error is raised after the response body has
|
||||
# started, the status should not change to an error status.
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('nonconfidential')
|
||||
else:
|
||||
# Because this error is raised after the response body has
|
||||
# started, and because it's chunked output, an error is raised by
|
||||
# the HTTP client when it encounters incomplete output.
|
||||
self.assertRaises((ValueError, IncompleteRead), self.getPage,
|
||||
'/demo/errinstream?id=5')
|
||||
# If this fails, then on_end_request isn't being called at all.
|
||||
time.sleep(0.1)
|
||||
self.getPage('/demo/ended/5')
|
||||
self.assertBody('True')
|
||||
|
||||
# Test the "__call__" technique (compile-time decorator).
|
||||
self.getPage('/demo/restricted')
|
||||
self.assertErrorPage(401)
|
||||
|
||||
# Test compile-time decorator with kwargs from config.
|
||||
self.getPage('/demo/userid')
|
||||
self.assertBody('Welcome!')
|
||||
|
||||
def testEndRequestOnDrop(self):
|
||||
old_timeout = None
|
||||
try:
|
||||
httpserver = cherrypy.server.httpserver
|
||||
old_timeout = httpserver.timeout
|
||||
except (AttributeError, IndexError):
|
||||
return self.skip()
|
||||
|
||||
try:
|
||||
httpserver.timeout = timeout
|
||||
|
||||
# Test that on_end_request is called even if the client drops.
|
||||
self.persistent = True
|
||||
try:
|
||||
conn = self.HTTP_CONN
|
||||
conn.putrequest('GET', '/demo/stream?id=9', skip_host=True)
|
||||
conn.putheader('Host', self.HOST)
|
||||
conn.endheaders()
|
||||
# Skip the rest of the request and close the conn. This will
|
||||
# cause the server's active socket to error, which *should*
|
||||
# result in the request being aborted, and request.close being
|
||||
# called all the way up the stack (including WSGI middleware),
|
||||
# eventually calling our on_end_request hook.
|
||||
finally:
|
||||
self.persistent = False
|
||||
time.sleep(timeout * 2)
|
||||
# Test that the on_end_request hook was called.
|
||||
self.getPage('/demo/ended/9')
|
||||
self.assertBody('True')
|
||||
finally:
|
||||
if old_timeout is not None:
|
||||
httpserver.timeout = old_timeout
|
||||
|
||||
def testGuaranteedHooks(self):
|
||||
# The 'critical' on_start_resource hook is 'failsafe' (guaranteed
|
||||
# to run even if there are failures in other on_start methods).
|
||||
# This is NOT true of the other hooks.
|
||||
# Here, we have set up a failure in NumerifyTool.numerify_map,
|
||||
# but our 'critical' hook should run and set the error to 502.
|
||||
self.getPage('/demo/err_in_onstart')
|
||||
self.assertErrorPage(502)
|
||||
tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
|
||||
expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems')
|
||||
self.assertInBody(expected_msg)
|
||||
|
||||
def testCombinedTools(self):
|
||||
expectedResult = (ntou('Hello,world') +
|
||||
europoundUnicode).encode('utf-8')
|
||||
zbuf = io.BytesIO()
|
||||
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
|
||||
zfile.write(expectedResult)
|
||||
zfile.close()
|
||||
|
||||
self.getPage('/euro',
|
||||
headers=[
|
||||
('Accept-Encoding', 'gzip'),
|
||||
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
|
||||
self.assertInBody(zbuf.getvalue()[:3])
|
||||
|
||||
zbuf = io.BytesIO()
|
||||
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
|
||||
zfile.write(expectedResult)
|
||||
zfile.close()
|
||||
|
||||
self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')])
|
||||
self.assertInBody(zbuf.getvalue()[:3])
|
||||
|
||||
# This returns a different value because gzip's priority was
|
||||
# lowered in conf, allowing the rotator to run after gzip.
|
||||
# Of course, we don't want breakage in production apps,
|
||||
# but it proves the priority was changed.
|
||||
self.getPage('/decorated_euro/subpath',
|
||||
headers=[('Accept-Encoding', 'gzip')])
|
||||
if six.PY3:
|
||||
self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
|
||||
else:
|
||||
self.assertInBody(''.join([chr((ord(x) + 3) % 256)
|
||||
for x in zbuf.getvalue()]))
|
||||
|
||||
def testBareHooks(self):
|
||||
content = 'bit of a pain in me gulliver'
|
||||
self.getPage('/pipe',
|
||||
headers=[('Content-Length', str(len(content))),
|
||||
('Content-Type', 'text/plain')],
|
||||
method='POST', body=content)
|
||||
self.assertBody(content)
|
||||
|
||||
def testHandlerWrapperTool(self):
|
||||
self.getPage('/tarfile')
|
||||
self.assertBody('I am a tarfile')
|
||||
|
||||
def testToolWithConfig(self):
|
||||
if not sys.version_info >= (2, 5):
|
||||
return self.skip('skipped (Python 2.5+ only)')
|
||||
|
||||
self.getPage('/tooldecs/blah')
|
||||
self.assertHeader('Content-Type', 'application/data')
|
||||
|
||||
def testWarnToolOn(self):
|
||||
# get
|
||||
try:
|
||||
cherrypy.tools.numerify.on
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Tool.on did not error as it should have.')
|
||||
|
||||
# set
|
||||
try:
|
||||
cherrypy.tools.numerify.on = True
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Tool.on did not error as it should have.')
|
||||
|
||||
def testDecorator(self):
|
||||
@cherrypy.tools.register('on_start_resource')
|
||||
def example():
|
||||
pass
|
||||
self.assertTrue(isinstance(cherrypy.tools.example, cherrypy.Tool))
|
||||
self.assertEqual(cherrypy.tools.example._point, 'on_start_resource')
|
||||
|
||||
@cherrypy.tools.register( # noqa: F811
|
||||
'before_finalize', name='renamed', priority=60,
|
||||
)
|
||||
def example():
|
||||
pass
|
||||
self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
|
||||
self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
|
||||
self.assertEqual(cherrypy.tools.renamed._name, 'renamed')
|
||||
self.assertEqual(cherrypy.tools.renamed._priority, 60)
|
||||
|
||||
|
||||
class SessionAuthTest(unittest.TestCase):
|
||||
|
||||
def test_login_screen_returns_bytes(self):
|
||||
"""
|
||||
login_screen must return bytes even if unicode parameters are passed.
|
||||
Issue 1132 revealed that login_screen would return unicode if the
|
||||
username and password were unicode.
|
||||
"""
|
||||
sa = cherrypy.lib.cptools.SessionAuth()
|
||||
res = sa.login_screen(None, username=six.text_type('nobody'),
|
||||
password=six.text_type('anypass'))
|
||||
self.assertTrue(isinstance(res, bytes))
|
||||
|
||||
|
||||
class TestHooks:
|
||||
def test_priorities(self):
|
||||
"""
|
||||
Hooks should sort by priority order.
|
||||
"""
|
||||
Hook = cherrypy._cprequest.Hook
|
||||
hooks = [
|
||||
Hook(None, priority=48),
|
||||
Hook(None),
|
||||
Hook(None, priority=49),
|
||||
]
|
||||
hooks.sort()
|
||||
by_priority = operator.attrgetter('priority')
|
||||
priorities = list(map(by_priority, hooks))
|
||||
assert priorities == [48, 49, 50]
|
||||
210
lib/cherrypy/test/test_tutorials.py
Normal file
210
lib/cherrypy/test/test_tutorials.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import sys
|
||||
import imp
|
||||
import types
|
||||
import importlib
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class TutorialTest(helper.CPWebCase):
|
||||
|
||||
@classmethod
|
||||
def setup_server(cls):
|
||||
"""
|
||||
Mount something so the engine starts.
|
||||
"""
|
||||
class Dummy:
|
||||
pass
|
||||
cherrypy.tree.mount(Dummy())
|
||||
|
||||
@staticmethod
|
||||
def load_module(name):
|
||||
"""
|
||||
Import or reload tutorial module as needed.
|
||||
"""
|
||||
target = 'cherrypy.tutorial.' + name
|
||||
if target in sys.modules:
|
||||
module = imp.reload(sys.modules[target])
|
||||
else:
|
||||
module = importlib.import_module(target)
|
||||
return module
|
||||
|
||||
@classmethod
|
||||
def setup_tutorial(cls, name, root_name, config={}):
|
||||
cherrypy.config.reset()
|
||||
module = cls.load_module(name)
|
||||
root = getattr(module, root_name)
|
||||
conf = getattr(module, 'tutconf')
|
||||
class_types = type,
|
||||
if six.PY2:
|
||||
class_types += types.ClassType,
|
||||
if isinstance(root, class_types):
|
||||
root = root()
|
||||
cherrypy.tree.mount(root, config=conf)
|
||||
cherrypy.config.update(config)
|
||||
|
||||
def test01HelloWorld(self):
|
||||
self.setup_tutorial('tut01_helloworld', 'HelloWorld')
|
||||
self.getPage('/')
|
||||
self.assertBody('Hello world!')
|
||||
|
||||
def test02ExposeMethods(self):
|
||||
self.setup_tutorial('tut02_expose_methods', 'HelloWorld')
|
||||
self.getPage('/show_msg')
|
||||
self.assertBody('Hello world!')
|
||||
|
||||
def test03GetAndPost(self):
|
||||
self.setup_tutorial('tut03_get_and_post', 'WelcomePage')
|
||||
|
||||
# Try different GET queries
|
||||
self.getPage('/greetUser?name=Bob')
|
||||
self.assertBody("Hey Bob, what's up?")
|
||||
|
||||
self.getPage('/greetUser')
|
||||
self.assertBody('Please enter your name <a href="./">here</a>.')
|
||||
|
||||
self.getPage('/greetUser?name=')
|
||||
self.assertBody('No, really, enter your name <a href="./">here</a>.')
|
||||
|
||||
# Try the same with POST
|
||||
self.getPage('/greetUser', method='POST', body='name=Bob')
|
||||
self.assertBody("Hey Bob, what's up?")
|
||||
|
||||
self.getPage('/greetUser', method='POST', body='name=')
|
||||
self.assertBody('No, really, enter your name <a href="./">here</a>.')
|
||||
|
||||
def test04ComplexSite(self):
|
||||
self.setup_tutorial('tut04_complex_site', 'root')
|
||||
|
||||
msg = '''
|
||||
<p>Here are some extra useful links:</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="http://del.icio.us">del.icio.us</a></li>
|
||||
<li><a href="http://www.cherrypy.org">CherryPy</a></li>
|
||||
</ul>
|
||||
|
||||
<p>[<a href="../">Return to links page</a>]</p>'''
|
||||
self.getPage('/links/extra/')
|
||||
self.assertBody(msg)
|
||||
|
||||
def test05DerivedObjects(self):
|
||||
self.setup_tutorial('tut05_derived_objects', 'HomePage')
|
||||
msg = '''
|
||||
<html>
|
||||
<head>
|
||||
<title>Another Page</title>
|
||||
<head>
|
||||
<body>
|
||||
<h2>Another Page</h2>
|
||||
|
||||
<p>
|
||||
And this is the amazing second page!
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
# the tutorial has some annoying spaces in otherwise blank lines
|
||||
msg = msg.replace('</h2>\n\n', '</h2>\n \n')
|
||||
msg = msg.replace('</p>\n\n', '</p>\n \n')
|
||||
self.getPage('/another/')
|
||||
self.assertBody(msg)
|
||||
|
||||
def test06DefaultMethod(self):
|
||||
self.setup_tutorial('tut06_default_method', 'UsersPage')
|
||||
self.getPage('/hendrik')
|
||||
self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
|
||||
'(<a href="./">back</a>)')
|
||||
|
||||
def test07Sessions(self):
|
||||
self.setup_tutorial('tut07_sessions', 'HitCounter')
|
||||
|
||||
self.getPage('/')
|
||||
self.assertBody(
|
||||
"\n During your current session, you've viewed this"
|
||||
'\n page 1 times! Your life is a patio of fun!'
|
||||
'\n ')
|
||||
|
||||
self.getPage('/', self.cookies)
|
||||
self.assertBody(
|
||||
"\n During your current session, you've viewed this"
|
||||
'\n page 2 times! Your life is a patio of fun!'
|
||||
'\n ')
|
||||
|
||||
def test08GeneratorsAndYield(self):
|
||||
self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo')
|
||||
self.getPage('/')
|
||||
self.assertBody('<html><body><h2>Generators rule!</h2>'
|
||||
'<h3>List of users:</h3>'
|
||||
'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
|
||||
'</body></html>')
|
||||
|
||||
def test09Files(self):
|
||||
self.setup_tutorial('tut09_files', 'FileDemo')
|
||||
|
||||
# Test upload
|
||||
filesize = 5
|
||||
h = [('Content-type', 'multipart/form-data; boundary=x'),
|
||||
('Content-Length', str(105 + filesize))]
|
||||
b = ('--x\n'
|
||||
'Content-Disposition: form-data; name="myFile"; '
|
||||
'filename="hello.txt"\r\n'
|
||||
'Content-Type: text/plain\r\n'
|
||||
'\r\n')
|
||||
b += 'a' * filesize + '\n' + '--x--\n'
|
||||
self.getPage('/upload', h, 'POST', b)
|
||||
self.assertBody('''<html>
|
||||
<body>
|
||||
myFile length: %d<br />
|
||||
myFile filename: hello.txt<br />
|
||||
myFile mime-type: text/plain
|
||||
</body>
|
||||
</html>''' % filesize)
|
||||
|
||||
# Test download
|
||||
self.getPage('/download')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'application/x-download')
|
||||
self.assertHeader('Content-Disposition',
|
||||
# Make sure the filename is quoted.
|
||||
'attachment; filename="pdf_file.pdf"')
|
||||
self.assertEqual(len(self.body), 85698)
|
||||
|
||||
def test10HTTPErrors(self):
|
||||
self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo')
|
||||
|
||||
@cherrypy.expose
|
||||
def traceback_setting():
|
||||
return repr(cherrypy.request.show_tracebacks)
|
||||
cherrypy.tree.mount(traceback_setting, '/traceback_setting')
|
||||
|
||||
self.getPage('/')
|
||||
self.assertInBody("""<a href="toggleTracebacks">""")
|
||||
self.assertInBody("""<a href="/doesNotExist">""")
|
||||
self.assertInBody("""<a href="/error?code=403">""")
|
||||
self.assertInBody("""<a href="/error?code=500">""")
|
||||
self.assertInBody("""<a href="/messageArg">""")
|
||||
|
||||
self.getPage('/traceback_setting')
|
||||
setting = self.body
|
||||
self.getPage('/toggleTracebacks')
|
||||
self.assertStatus((302, 303))
|
||||
self.getPage('/traceback_setting')
|
||||
self.assertBody(str(not eval(setting)))
|
||||
|
||||
self.getPage('/error?code=500')
|
||||
self.assertStatus(500)
|
||||
self.assertInBody('The server encountered an unexpected condition '
|
||||
'which prevented it from fulfilling the request.')
|
||||
|
||||
self.getPage('/error?code=403')
|
||||
self.assertStatus(403)
|
||||
self.assertInBody("<h2>You can't do that!</h2>")
|
||||
|
||||
self.getPage('/messageArg')
|
||||
self.assertStatus(500)
|
||||
self.assertInBody("If you construct an HTTPError with a 'message'")
|
||||
113
lib/cherrypy/test/test_virtualhost.py
Normal file
113
lib/cherrypy/test/test_virtualhost.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import os
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
class VirtualHostTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hello, world'
|
||||
|
||||
@cherrypy.expose
|
||||
def dom4(self):
|
||||
return 'Under construction'
|
||||
|
||||
@cherrypy.expose
|
||||
def method(self, value):
|
||||
return 'You sent %s' % value
|
||||
|
||||
class VHost:
|
||||
|
||||
def __init__(self, sitename):
|
||||
self.sitename = sitename
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Welcome to %s' % self.sitename
|
||||
|
||||
@cherrypy.expose
|
||||
def vmethod(self, value):
|
||||
return 'You sent %s' % value
|
||||
|
||||
@cherrypy.expose
|
||||
def url(self):
|
||||
return cherrypy.url('nextpage')
|
||||
|
||||
# Test static as a handler (section must NOT include vhost prefix)
|
||||
static = cherrypy.tools.staticdir.handler(
|
||||
section='/static', dir=curdir)
|
||||
|
||||
root = Root()
|
||||
root.mydom2 = VHost('Domain 2')
|
||||
root.mydom3 = VHost('Domain 3')
|
||||
hostmap = {'www.mydom2.com': '/mydom2',
|
||||
'www.mydom3.com': '/mydom3',
|
||||
'www.mydom4.com': '/dom4',
|
||||
}
|
||||
cherrypy.tree.mount(root, config={
|
||||
'/': {
|
||||
'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)
|
||||
},
|
||||
# Test static in config (section must include vhost prefix)
|
||||
'/mydom2/static2': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.root': curdir,
|
||||
'tools.staticdir.dir': 'static',
|
||||
'tools.staticdir.index': 'index.html',
|
||||
},
|
||||
})
|
||||
|
||||
def testVirtualHost(self):
|
||||
self.getPage('/', [('Host', 'www.mydom1.com')])
|
||||
self.assertBody('Hello, world')
|
||||
self.getPage('/mydom2/', [('Host', 'www.mydom1.com')])
|
||||
self.assertBody('Welcome to Domain 2')
|
||||
|
||||
self.getPage('/', [('Host', 'www.mydom2.com')])
|
||||
self.assertBody('Welcome to Domain 2')
|
||||
self.getPage('/', [('Host', 'www.mydom3.com')])
|
||||
self.assertBody('Welcome to Domain 3')
|
||||
self.getPage('/', [('Host', 'www.mydom4.com')])
|
||||
self.assertBody('Under construction')
|
||||
|
||||
# Test GET, POST, and positional params
|
||||
self.getPage('/method?value=root')
|
||||
self.assertBody('You sent root')
|
||||
self.getPage('/vmethod?value=dom2+GET', [('Host', 'www.mydom2.com')])
|
||||
self.assertBody('You sent dom2 GET')
|
||||
self.getPage('/vmethod', [('Host', 'www.mydom3.com')], method='POST',
|
||||
body='value=dom3+POST')
|
||||
self.assertBody('You sent dom3 POST')
|
||||
self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')])
|
||||
self.assertBody('You sent pos')
|
||||
|
||||
# Test that cherrypy.url uses the browser url, not the virtual url
|
||||
self.getPage('/url', [('Host', 'www.mydom2.com')])
|
||||
self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme)
|
||||
|
||||
def test_VHost_plus_Static(self):
|
||||
# Test static as a handler
|
||||
self.getPage('/static/style.css', [('Host', 'www.mydom2.com')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeader('Content-Type', 'text/css;charset=utf-8')
|
||||
|
||||
# Test static in config
|
||||
self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg'])
|
||||
|
||||
# Test static config with "index" arg
|
||||
self.getPage('/static2/', [('Host', 'www.mydom2.com')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody('Hello, world\r\n')
|
||||
# Since tools.trailing_slash is on by default, this should redirect
|
||||
self.getPage('/static2', [('Host', 'www.mydom2.com')])
|
||||
self.assertStatus(301)
|
||||
93
lib/cherrypy/test/test_wsgi_ns.py
Normal file
93
lib/cherrypy/test/test_wsgi_ns.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class WSGI_Namespace_Test(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
class WSGIResponse(object):
|
||||
|
||||
def __init__(self, appresults):
|
||||
self.appresults = appresults
|
||||
self.iter = iter(appresults)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
return self.iter.next()
|
||||
|
||||
def __next__(self):
|
||||
return next(self.iter)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self.appresults, 'close'):
|
||||
self.appresults.close()
|
||||
|
||||
class ChangeCase(object):
|
||||
|
||||
def __init__(self, app, to=None):
|
||||
self.app = app
|
||||
self.to = to
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
res = self.app(environ, start_response)
|
||||
|
||||
class CaseResults(WSGIResponse):
|
||||
|
||||
def next(this):
|
||||
return getattr(this.iter.next(), self.to)()
|
||||
|
||||
def __next__(this):
|
||||
return getattr(next(this.iter), self.to)()
|
||||
return CaseResults(res)
|
||||
|
||||
class Replacer(object):
|
||||
|
||||
def __init__(self, app, map={}):
|
||||
self.app = app
|
||||
self.map = map
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
res = self.app(environ, start_response)
|
||||
|
||||
class ReplaceResults(WSGIResponse):
|
||||
|
||||
def next(this):
|
||||
line = this.iter.next()
|
||||
for k, v in self.map.iteritems():
|
||||
line = line.replace(k, v)
|
||||
return line
|
||||
|
||||
def __next__(this):
|
||||
line = next(this.iter)
|
||||
for k, v in self.map.items():
|
||||
line = line.replace(k, v)
|
||||
return line
|
||||
return ReplaceResults(res)
|
||||
|
||||
class Root(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'HellO WoRlD!'
|
||||
|
||||
root_conf = {'wsgi.pipeline': [('replace', Replacer)],
|
||||
'wsgi.replace.map': {b'L': b'X',
|
||||
b'l': b'r'},
|
||||
}
|
||||
|
||||
app = cherrypy.Application(Root())
|
||||
app.wsgiapp.pipeline.append(('changecase', ChangeCase))
|
||||
app.wsgiapp.config['changecase'] = {'to': 'upper'}
|
||||
cherrypy.tree.mount(app, config={'/': root_conf})
|
||||
|
||||
def test_pipeline(self):
|
||||
if not cherrypy.server.httpserver:
|
||||
return self.skip()
|
||||
|
||||
self.getPage('/')
|
||||
# If body is "HEXXO WORXD!", the middleware was applied out of order.
|
||||
self.assertBody('HERRO WORRD!')
|
||||
93
lib/cherrypy/test/test_wsgi_unix_socket.py
Normal file
93
lib/cherrypy/test/test_wsgi_unix_socket.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
import socket
|
||||
import atexit
|
||||
import tempfile
|
||||
|
||||
from six.moves.http_client import HTTPConnection
|
||||
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
def usocket_path():
|
||||
fd, path = tempfile.mkstemp('cp_test.sock')
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
return path
|
||||
|
||||
|
||||
USOCKET_PATH = usocket_path()
|
||||
|
||||
|
||||
class USocketHTTPConnection(HTTPConnection):
|
||||
"""
|
||||
HTTPConnection over a unix socket.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
HTTPConnection.__init__(self, 'localhost')
|
||||
self.path = path
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""
|
||||
Catch-all method just to present itself as a constructor for the
|
||||
HTTPConnection.
|
||||
"""
|
||||
return self
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Override the connect method and assign a unix socket as a transport.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(self.path)
|
||||
self.sock = sock
|
||||
atexit.register(lambda: os.remove(self.path))
|
||||
|
||||
|
||||
@pytest.mark.skipif("sys.platform == 'win32'")
|
||||
class WSGI_UnixSocket_Test(helper.CPWebCase):
|
||||
"""
|
||||
Test basic behavior on a cherrypy wsgi server listening
|
||||
on a unix socket.
|
||||
|
||||
It exercises the config option `server.socket_file`.
|
||||
"""
|
||||
HTTP_CONN = USocketHTTPConnection(USOCKET_PATH)
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Test OK'
|
||||
|
||||
@cherrypy.expose
|
||||
def error(self):
|
||||
raise Exception('Invalid page')
|
||||
|
||||
config = {
|
||||
'server.socket_file': USOCKET_PATH
|
||||
}
|
||||
cherrypy.config.update(config)
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
def tearDown(self):
|
||||
cherrypy.config.update({'server.socket_file': None})
|
||||
|
||||
def test_simple_request(self):
|
||||
self.getPage('/')
|
||||
self.assertStatus('200 OK')
|
||||
self.assertInBody('Test OK')
|
||||
|
||||
def test_not_found(self):
|
||||
self.getPage('/invalid_path')
|
||||
self.assertStatus('404 Not Found')
|
||||
|
||||
def test_internal_error(self):
|
||||
self.getPage('/error')
|
||||
self.assertStatus('500 Internal Server Error')
|
||||
self.assertInBody('Invalid page')
|
||||
35
lib/cherrypy/test/test_wsgi_vhost.py
Normal file
35
lib/cherrypy/test/test_wsgi_vhost.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class WSGI_VirtualHost_Test(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
class ClassOfRoot(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Welcome to the %s website!' % self.name
|
||||
|
||||
default = cherrypy.Application(None)
|
||||
|
||||
domains = {}
|
||||
for year in range(1997, 2008):
|
||||
app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
|
||||
domains['www.classof%s.example' % year] = app
|
||||
|
||||
cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
|
||||
|
||||
def test_welcome(self):
|
||||
if not cherrypy.server.using_wsgi:
|
||||
return self.skip('skipped (not using WSGI)... ')
|
||||
|
||||
for year in range(1997, 2008):
|
||||
self.getPage(
|
||||
'/', headers=[('Host', 'www.classof%s.example' % year)])
|
||||
self.assertBody('Welcome to the Class of %s website!' % year)
|
||||
120
lib/cherrypy/test/test_wsgiapps.py
Normal file
120
lib/cherrypy/test/test_wsgiapps.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class WSGIGraftTests(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
def test_app(environ, start_response):
|
||||
status = '200 OK'
|
||||
response_headers = [('Content-type', 'text/plain')]
|
||||
start_response(status, response_headers)
|
||||
output = ['Hello, world!\n',
|
||||
'This is a wsgi app running within CherryPy!\n\n']
|
||||
keys = list(environ.keys())
|
||||
keys.sort()
|
||||
for k in keys:
|
||||
output.append('%s: %s\n' % (k, environ[k]))
|
||||
return [ntob(x, 'utf-8') for x in output]
|
||||
|
||||
def test_empty_string_app(environ, start_response):
|
||||
status = '200 OK'
|
||||
response_headers = [('Content-type', 'text/plain')]
|
||||
start_response(status, response_headers)
|
||||
return [
|
||||
b'Hello', b'', b' ', b'', b'world',
|
||||
]
|
||||
|
||||
class WSGIResponse(object):
|
||||
|
||||
def __init__(self, appresults):
|
||||
self.appresults = appresults
|
||||
self.iter = iter(appresults)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
def __next__(self):
|
||||
return next(self.iter)
|
||||
else:
|
||||
def next(self):
|
||||
return self.iter.next()
|
||||
|
||||
def close(self):
|
||||
if hasattr(self.appresults, 'close'):
|
||||
self.appresults.close()
|
||||
|
||||
class ReversingMiddleware(object):
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
results = app(environ, start_response)
|
||||
|
||||
class Reverser(WSGIResponse):
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
def __next__(this):
|
||||
line = list(next(this.iter))
|
||||
line.reverse()
|
||||
return bytes(line)
|
||||
else:
|
||||
def next(this):
|
||||
line = list(this.iter.next())
|
||||
line.reverse()
|
||||
return ''.join(line)
|
||||
|
||||
return Reverser(results)
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return ntob("I'm a regular CherryPy page handler!")
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
cherrypy.tree.graft(test_app, '/hosted/app1')
|
||||
cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
|
||||
|
||||
# Set script_name explicitly to None to signal CP that it should
|
||||
# be pulled from the WSGI environ each time.
|
||||
app = cherrypy.Application(Root(), script_name=None)
|
||||
cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2')
|
||||
|
||||
wsgi_output = '''Hello, world!
|
||||
This is a wsgi app running within CherryPy!'''
|
||||
|
||||
def test_01_standard_app(self):
|
||||
self.getPage('/')
|
||||
self.assertBody("I'm a regular CherryPy page handler!")
|
||||
|
||||
def test_04_pure_wsgi(self):
|
||||
if not cherrypy.server.using_wsgi:
|
||||
return self.skip('skipped (not using WSGI)... ')
|
||||
self.getPage('/hosted/app1')
|
||||
self.assertHeader('Content-Type', 'text/plain')
|
||||
self.assertInBody(self.wsgi_output)
|
||||
|
||||
def test_05_wrapped_cp_app(self):
|
||||
if not cherrypy.server.using_wsgi:
|
||||
return self.skip('skipped (not using WSGI)... ')
|
||||
self.getPage('/hosted/app2/')
|
||||
body = list("I'm a regular CherryPy page handler!")
|
||||
body.reverse()
|
||||
body = ''.join(body)
|
||||
self.assertInBody(body)
|
||||
|
||||
def test_06_empty_string_app(self):
|
||||
if not cherrypy.server.using_wsgi:
|
||||
return self.skip('skipped (not using WSGI)... ')
|
||||
self.getPage('/hosted/app3')
|
||||
self.assertHeader('Content-Type', 'text/plain')
|
||||
self.assertInBody('Hello world')
|
||||
183
lib/cherrypy/test/test_xmlrpc.py
Normal file
183
lib/cherrypy/test/test_xmlrpc.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from six.moves.xmlrpc_client import (
|
||||
DateTime, Fault,
|
||||
ProtocolError, ServerProxy, SafeTransport
|
||||
)
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cptools
|
||||
from cherrypy.test import helper
|
||||
|
||||
if six.PY3:
|
||||
HTTPSTransport = SafeTransport
|
||||
|
||||
# Python 3.0's SafeTransport still mistakenly checks for socket.ssl
|
||||
import socket
|
||||
if not hasattr(socket, 'ssl'):
|
||||
socket.ssl = True
|
||||
else:
|
||||
class HTTPSTransport(SafeTransport):
|
||||
|
||||
"""Subclass of SafeTransport to fix sock.recv errors (by using file).
|
||||
"""
|
||||
|
||||
def request(self, host, handler, request_body, verbose=0):
|
||||
# issue XML-RPC request
|
||||
h = self.make_connection(host)
|
||||
if verbose:
|
||||
h.set_debuglevel(1)
|
||||
|
||||
self.send_request(h, handler, request_body)
|
||||
self.send_host(h, host)
|
||||
self.send_user_agent(h)
|
||||
self.send_content(h, request_body)
|
||||
|
||||
errcode, errmsg, headers = h.getreply()
|
||||
if errcode != 200:
|
||||
raise ProtocolError(host + handler, errcode, errmsg, headers)
|
||||
|
||||
self.verbose = verbose
|
||||
|
||||
# Here's where we differ from the superclass. It says:
|
||||
# try:
|
||||
# sock = h._conn.sock
|
||||
# except AttributeError:
|
||||
# sock = None
|
||||
# return self._parse_response(h.getfile(), sock)
|
||||
|
||||
return self.parse_response(h.getfile())
|
||||
|
||||
|
||||
def setup_server():
|
||||
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "I'm a standard index!"
|
||||
|
||||
class XmlRpc(_cptools.XMLRPCController):
|
||||
|
||||
@cherrypy.expose
|
||||
def foo(self):
|
||||
return 'Hello world!'
|
||||
|
||||
@cherrypy.expose
|
||||
def return_single_item_list(self):
|
||||
return [42]
|
||||
|
||||
@cherrypy.expose
|
||||
def return_string(self):
|
||||
return 'here is a string'
|
||||
|
||||
@cherrypy.expose
|
||||
def return_tuple(self):
|
||||
return ('here', 'is', 1, 'tuple')
|
||||
|
||||
@cherrypy.expose
|
||||
def return_dict(self):
|
||||
return dict(a=1, b=2, c=3)
|
||||
|
||||
@cherrypy.expose
|
||||
def return_composite(self):
|
||||
return dict(a=1, z=26), 'hi', ['welcome', 'friend']
|
||||
|
||||
@cherrypy.expose
|
||||
def return_int(self):
|
||||
return 42
|
||||
|
||||
@cherrypy.expose
|
||||
def return_float(self):
|
||||
return 3.14
|
||||
|
||||
@cherrypy.expose
|
||||
def return_datetime(self):
|
||||
return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
|
||||
|
||||
@cherrypy.expose
|
||||
def return_boolean(self):
|
||||
return True
|
||||
|
||||
@cherrypy.expose
|
||||
def test_argument_passing(self, num):
|
||||
return num * 2
|
||||
|
||||
@cherrypy.expose
|
||||
def test_returning_Fault(self):
|
||||
return Fault(1, 'custom Fault response')
|
||||
|
||||
root = Root()
|
||||
root.xmlrpc = XmlRpc()
|
||||
cherrypy.tree.mount(root, config={'/': {
|
||||
'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
|
||||
'tools.xmlrpc.allow_none': 0,
|
||||
}})
|
||||
|
||||
|
||||
class XmlRpcTest(helper.CPWebCase):
|
||||
setup_server = staticmethod(setup_server)
|
||||
|
||||
def testXmlRpc(self):
|
||||
|
||||
scheme = self.scheme
|
||||
if scheme == 'https':
|
||||
url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
|
||||
proxy = ServerProxy(url, transport=HTTPSTransport())
|
||||
else:
|
||||
url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
|
||||
proxy = ServerProxy(url)
|
||||
|
||||
# begin the tests ...
|
||||
self.getPage('/xmlrpc/foo')
|
||||
self.assertBody('Hello world!')
|
||||
|
||||
self.assertEqual(proxy.return_single_item_list(), [42])
|
||||
self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
|
||||
self.assertEqual(proxy.return_string(), 'here is a string')
|
||||
self.assertEqual(proxy.return_tuple(),
|
||||
list(('here', 'is', 1, 'tuple')))
|
||||
self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
|
||||
self.assertEqual(proxy.return_composite(),
|
||||
[{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
|
||||
self.assertEqual(proxy.return_int(), 42)
|
||||
self.assertEqual(proxy.return_float(), 3.14)
|
||||
self.assertEqual(proxy.return_datetime(),
|
||||
DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
|
||||
self.assertEqual(proxy.return_boolean(), True)
|
||||
self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
|
||||
|
||||
# Test an error in the page handler (should raise an xmlrpclib.Fault)
|
||||
try:
|
||||
proxy.test_argument_passing({})
|
||||
except Exception:
|
||||
x = sys.exc_info()[1]
|
||||
self.assertEqual(x.__class__, Fault)
|
||||
self.assertEqual(x.faultString, ('unsupported operand type(s) '
|
||||
"for *: 'dict' and 'int'"))
|
||||
else:
|
||||
self.fail('Expected xmlrpclib.Fault')
|
||||
|
||||
# https://github.com/cherrypy/cherrypy/issues/533
|
||||
# if a method is not found, an xmlrpclib.Fault should be raised
|
||||
try:
|
||||
proxy.non_method()
|
||||
except Exception:
|
||||
x = sys.exc_info()[1]
|
||||
self.assertEqual(x.__class__, Fault)
|
||||
self.assertEqual(x.faultString,
|
||||
'method "non_method" is not supported')
|
||||
else:
|
||||
self.fail('Expected xmlrpclib.Fault')
|
||||
|
||||
# Test returning a Fault from the page handler.
|
||||
try:
|
||||
proxy.test_returning_Fault()
|
||||
except Exception:
|
||||
x = sys.exc_info()[1]
|
||||
self.assertEqual(x.__class__, Fault)
|
||||
self.assertEqual(x.faultString, ('custom Fault response'))
|
||||
else:
|
||||
self.fail('Expected xmlrpclib.Fault')
|
||||
11
lib/cherrypy/test/webtest.py
Normal file
11
lib/cherrypy/test/webtest.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# for compatibility, expose cheroot webtest here
|
||||
import warnings
|
||||
|
||||
from cheroot.test.webtest import ( # noqa
|
||||
interface,
|
||||
WebCase, cleanHeaders, shb, openURL,
|
||||
ServerError, server_error,
|
||||
)
|
||||
|
||||
|
||||
warnings.warn('Use cheroot.test.webtest', DeprecationWarning)
|
||||
Reference in New Issue
Block a user