Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/cherrypy/test
diff options
context:
space:
mode:
Diffstat (limited to 'cherrypy/test')
-rwxr-xr-xcherrypy/test/__init__.py25
-rwxr-xr-xcherrypy/test/_test_decorators.py41
-rwxr-xr-xcherrypy/test/_test_states_demo.py66
-rwxr-xr-xcherrypy/test/benchmark.py409
-rwxr-xr-xcherrypy/test/checkerdemo.py47
-rwxr-xr-xcherrypy/test/helper.py476
-rwxr-xr-xcherrypy/test/logtest.py181
-rwxr-xr-xcherrypy/test/modfastcgi.py135
-rwxr-xr-xcherrypy/test/modfcgid.py125
-rwxr-xr-xcherrypy/test/modpy.py163
-rwxr-xr-xcherrypy/test/modwsgi.py148
-rwxr-xr-xcherrypy/test/sessiondemo.py153
-rw-r--r--cherrypy/test/static/dirback.jpgbin0 -> 18238 bytes
-rw-r--r--cherrypy/test/static/index.html1
-rw-r--r--cherrypy/test/style.css1
-rw-r--r--cherrypy/test/test.pem38
-rwxr-xr-xcherrypy/test/test_auth_basic.py79
-rwxr-xr-xcherrypy/test/test_auth_digest.py115
-rwxr-xr-xcherrypy/test/test_bus.py263
-rwxr-xr-xcherrypy/test/test_caching.py329
-rwxr-xr-xcherrypy/test/test_config.py249
-rwxr-xr-xcherrypy/test/test_config_server.py121
-rwxr-xr-xcherrypy/test/test_conn.py734
-rwxr-xr-xcherrypy/test/test_core.py617
-rwxr-xr-xcherrypy/test/test_dynamicobjectmapping.py403
-rwxr-xr-xcherrypy/test/test_encoding.py363
-rwxr-xr-xcherrypy/test/test_etags.py81
-rwxr-xr-xcherrypy/test/test_http.py168
-rwxr-xr-xcherrypy/test/test_httpauth.py151
-rwxr-xr-xcherrypy/test/test_httplib.py29
-rwxr-xr-xcherrypy/test/test_json.py79
-rwxr-xr-xcherrypy/test/test_logging.py149
-rwxr-xr-xcherrypy/test/test_mime.py128
-rwxr-xr-xcherrypy/test/test_misc_tools.py202
-rwxr-xr-xcherrypy/test/test_objectmapping.py403
-rwxr-xr-xcherrypy/test/test_proxy.py129
-rwxr-xr-xcherrypy/test/test_refleaks.py119
-rwxr-xr-xcherrypy/test/test_request_obj.py722
-rwxr-xr-xcherrypy/test/test_routes.py69
-rwxr-xr-xcherrypy/test/test_session.py464
-rwxr-xr-xcherrypy/test/test_sessionauthenticate.py62
-rwxr-xr-xcherrypy/test/test_states.py436
-rwxr-xr-xcherrypy/test/test_static.py300
-rwxr-xr-xcherrypy/test/test_tools.py393
-rwxr-xr-xcherrypy/test/test_tutorials.py201
-rwxr-xr-xcherrypy/test/test_virtualhost.py107
-rwxr-xr-xcherrypy/test/test_wsgi_ns.py80
-rwxr-xr-xcherrypy/test/test_wsgi_vhost.py36
-rwxr-xr-xcherrypy/test/test_wsgiapps.py111
-rwxr-xr-xcherrypy/test/test_xmlrpc.py172
-rwxr-xr-xcherrypy/test/webtest.py535
51 files changed, 10608 insertions, 0 deletions
diff --git a/cherrypy/test/__init__.py b/cherrypy/test/__init__.py
new file mode 100755
index 0000000..e4c400d
--- /dev/null
+++ b/cherrypy/test/__init__.py
@@ -0,0 +1,25 @@
+"""Regression test suite for CherryPy.
+
+Run 'nosetests -s test/' to exercise all tests.
+
+The '-s' flag instructs nose to output stdout messages, wihch is crucial to
+the 'interactive' mode of webtest.py. If you run these tests without the '-s'
+flag, don't be surprised if the test seems to hang: it's waiting for your
+interactive input.
+"""
+
+import sys
+def newexit():
+ raise SystemExit('Exit called')
+
+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
diff --git a/cherrypy/test/_test_decorators.py b/cherrypy/test/_test_decorators.py
new file mode 100755
index 0000000..5bcbc1e
--- /dev/null
+++ b/cherrypy/test/_test_decorators.py
@@ -0,0 +1,41 @@
+"""Test module for the @-decorator syntax, which is version-specific"""
+
+from cherrypy import expose, tools
+from cherrypy._cpcompat import ntob
+
+
+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
+ @tools.response_headers(headers=[('Content-Type', 'application/data')])
+ def blah(self):
+ yield ntob("blah")
+ # This is here to demonstrate that _cp_config = {...} overwrites
+ # the _cp_config attribute added by the Tool decorator. You have
+ # to write _cp_config[k] = v or _cp_config.update(...) instead.
+ blah._cp_config['response.stream'] = True
+
+
diff --git a/cherrypy/test/_test_states_demo.py b/cherrypy/test/_test_states_demo.py
new file mode 100755
index 0000000..3f8f196
--- /dev/null
+++ b/cherrypy/test/_test_states_demo.py
@@ -0,0 +1,66 @@
+import os
+import sys
+import time
+starttime = time.time()
+
+import cherrypy
+
+
+class Root:
+
+ def index(self):
+ return "Hello World"
+ index.exposed = True
+
+ def mtimes(self):
+ return repr(cherrypy.engine.publish("Autoreloader", "mtimes"))
+ mtimes.exposed = True
+
+ def pid(self):
+ return str(os.getpid())
+ pid.exposed = True
+
+ def start(self):
+ return repr(starttime)
+ start.exposed = True
+
+ 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()
+ exit.exposed = True
+
+
+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', unsub_sig, priority=100)
+
+
+def starterror():
+ if cherrypy.config.get('starterror', False):
+ zerodiv = 1 / 0
+cherrypy.engine.subscribe('start', starterror, 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.engine.subscribe('start', log_test_case_name, priority=6)
+
+
+cherrypy.tree.mount(Root(), '/', {'/': {}})
diff --git a/cherrypy/test/benchmark.py b/cherrypy/test/benchmark.py
new file mode 100755
index 0000000..90536a5
--- /dev/null
+++ b/cherrypy/test/benchmark.py
@@ -0,0 +1,409 @@
+"""CherryPy Benchmark Tool
+
+ Usage:
+ benchmark.py --null --notests --help --cpmodpy --modpython --ab=path --apache=path
+
+ --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 8080 (with the builtin _cpmodpy)
+ --modpython: run tests via apache on 8080 (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
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import re
+import sys
+import time
+import traceback
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy import _cperror, _cpmodpy
+from cherrypy.lib import httputil
+
+
+AB_PATH = ""
+APACHE_PATH = "apache"
+SCRIPT_NAME = "/cpbench/users/rdelon/apps/blog"
+
+__all__ = ['ABSession', 'Root', 'print_report',
+ 'run_standard_benchmarks', 'safe_threads',
+ 'size_report', 'startup', 'thread_report',
+ ]
+
+size_cache = {}
+
+class Root:
+
+ 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>"""
+ index.exposed = True
+
+ def hello(self):
+ return "Hello, world\r\n"
+ hello.exposed = True
+
+ def sizer(self, size):
+ resp = size_cache.get(size, None)
+ if resp is None:
+ size_cache[size] = resp = "X" * int(size)
+ return resp
+ sizer.exposed = True
+
+
+cherrypy.config.update({
+ 'log.error.file': '',
+ 'environment': 'production',
+ 'server.socket_host': '127.0.0.1',
+ 'server.socket_port': 8080,
+ 'server.max_request_header_size': 0,
+ 'server.max_request_body_size': 0,
+ 'engine.deadlock_poll_freq': 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,
+ },
+ }
+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: 8080
+
+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',
+ ntob(r'^Complete requests:\s*(\d+)')),
+ ('failed_requests', 'Failed',
+ ntob(r'^Failed requests:\s*(\d+)')),
+ ('requests_per_second', 'req/sec',
+ ntob(r'^Requests per second:\s*([0-9.]+)')),
+ ('time_per_request_concurrent', 'msec/req',
+ ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
+ ('transfer_rate', 'KB/sec',
+ ntob(r'^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:
+ 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 i, val in enumerate(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=8080, opts=pyopts, apache_path=APACHE_PATH, handler=handler)
+ else:
+ pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython"))
+ s = s(port=8080, opts=pyopts, apache_path=APACHE_PATH)
+
+ try:
+ s.start()
+ run()
+ finally:
+ s.stop()
+
+
+
+if __name__ == '__main__':
+ 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:
+ 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()
diff --git a/cherrypy/test/checkerdemo.py b/cherrypy/test/checkerdemo.py
new file mode 100755
index 0000000..32a7dee
--- /dev/null
+++ b/cherrypy/test/checkerdemo.py
@@ -0,0 +1,47 @@
+"""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)
diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py
new file mode 100755
index 0000000..ff9e06c
--- /dev/null
+++ b/cherrypy/test/helper.py
@@ -0,0 +1,476 @@
+"""A library of helper functions for the CherryPy test suite."""
+
+import datetime
+import logging
+log = logging.getLogger(__name__)
+import os
+thisdir = os.path.abspath(os.path.dirname(__file__))
+serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
+
+import re
+import sys
+import time
+import warnings
+
+import cherrypy
+from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob
+from cherrypy.lib import httputil
+from cherrypy.lib.reprconf import unrepr
+from cherrypy.test import webtest
+
+import nose
+
+_testconfig = None
+
+def get_tst_config(overconf = {}):
+ global _testconfig
+ if _testconfig is None:
+ conf = {
+ 'scheme': 'http',
+ 'protocol': "HTTP/1.1",
+ 'port': 8080,
+ 'host': '127.0.0.1',
+ 'validate': False,
+ 'conquer': False,
+ 'server': 'wsgi',
+ }
+ try:
+ import testconfig
+ _conf = testconfig.config.get('supervisor', None)
+ if _conf is not None:
+ for k, v in _conf.items():
+ if isinstance(v, basestring):
+ _conf[k] = unrepr(v)
+ conf.update(_conf)
+ except ImportError:
+ pass
+ _testconfig = conf
+ conf = _testconfig.copy()
+ conf.update(overconf)
+
+ return conf
+
+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)
+
+
+log_to_stderr = lambda msg, level: 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
+
+ engine = cherrypy.engine
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.subscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.subscribe()
+ #engine.subscribe('log', log_to_stderr)
+
+ 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()
+
+ for name, server in copyitems(getattr(cherrypy, 'servers', {})):
+ 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.conquer:
+ try:
+ import wsgiconq
+ except ImportError:
+ warnings.warn("Error importing wsgiconq. pyconquer will not run.")
+ else:
+ app = wsgiconq.WSGILogger(app, c_calls=True)
+
+ 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"
+
+ 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, basestring):
+ 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
+ _setup_server = classmethod(_setup_server)
+
+ def setup_class(cls):
+ ''
+ #Creates a server
+ conf = get_tst_config()
+ 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()
+ supervisor.start(cls.__module__)
+
+ cls.supervisor = supervisor
+ setup_class = classmethod(setup_class)
+
+ def teardown_class(cls):
+ ''
+ if hasattr(cls, 'setup_server'):
+ cls.supervisor.stop()
+ teardown_class = classmethod(teardown_class)
+
+ 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):
+ """Open the url. Return status, headers, body."""
+ if self.script_name:
+ url = httputil.urljoin(self.script_name, url)
+ return webtest.WebCase.getPage(self, url, headers, method, body, protocol)
+
+ def skip(self, msg='skipped '):
+ raise nose.SkipTest(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.
+ esc = re.escape
+ epage = esc(page)
+ epage = epage.replace(esc('<pre id="traceback"></pre>'),
+ esc('<pre id="traceback">') + '(.*)' + esc('</pre>'))
+ m = re.match(ntob(epage, self.encoding), 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 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,
+ }
+ f = open(self.config_file, 'wb')
+ f.write(ntob(conf, 'utf-8'))
+ f.close()
+
+ def start(self, imports=None):
+ """Start cherryd in a subprocess."""
+ cherrypy._cpserver.wait_for_free_port(self.host, self.port)
+
+ args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'),
+ '-c', self.config_file, '-p', self.pid_file]
+
+ 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
+ if self.wait:
+ self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env)
+ else:
+ os.spawnve(os.P_NOWAIT, sys.executable, args, env)
+ cherrypy._cpserver.wait_for_occupied_port(self.host, self.port)
+
+ # 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):
+ return int(open(self.pid_file, 'rb').read())
+
+ def join(self):
+ """Wait for the process to exit."""
+ 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
+
diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py
new file mode 100755
index 0000000..c093da2
--- /dev/null
+++ b/cherrypy/test/logtest.py
@@ -0,0 +1,181 @@
+"""logtest, a unittest.TestCase helper for testing log output."""
+
+import sys
+import time
+
+import cherrypy
+
+
+try:
+ # On Windows, msvcrt.getch reads a single char without output.
+ import msvcrt
+ def getchar():
+ return msvcrt.getch()
+except ImportError:
+ # Unix getchr
+ import tty, 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 = "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 >> "
+ print 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
+ print "<-- More -->\r",
+ m = getchar().lower()
+ # Erase our "More" prompt
+ print " \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()
+ print 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("%s%s\n" % (self.markerPrefix, key))
+
+ 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()
+
+ data = []
+ in_region = False
+ for line in open(logfile, 'rb'):
+ if in_region:
+ if (line.startswith(self.markerPrefix) and not marker 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 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 lines not in data[sliceargs]:
+ msg = "%r not found on log line %r" % (lines, sliceargs)
+ self._handleLogError(msg, [data[sliceargs]], marker, lines)
+ else:
+ # Multiple args. Use __getslice__ and require lines to be list.
+ if isinstance(lines, tuple):
+ lines = list(lines)
+ elif isinstance(lines, basestring):
+ 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 line not in logline:
+ msg = "%r not found in log" % line
+ self._handleLogError(msg, data[start:stop], marker, line)
+
diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py
new file mode 100755
index 0000000..95acf14
--- /dev/null
+++ b/cherrypy/test/modfastcgi.py
@@ -0,0 +1,135 @@
+"""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
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy.test import helper
+
+
+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)
+
diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py
new file mode 100755
index 0000000..736aa4c
--- /dev/null
+++ b/cherrypy/test/modfcgid.py
@@ -0,0 +1,125 @@
+"""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
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.process import plugins, servers
+from cherrypy.test import helper
+
+
+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()
+
diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py
new file mode 100755
index 0000000..519571f
--- /dev/null
+++ b/cherrypy/test/modpy.py
@@ -0,0 +1,163 @@
+"""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
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import time
+
+from cherrypy.test import helper
+
+
+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()
+
+ import cherrypy
+ 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()
+
+ import cherrypy
+ 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
+
diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py
new file mode 100755
index 0000000..309a541
--- /dev/null
+++ b/cherrypy/test/modwsgi.py
@@ -0,0 +1,148 @@
+"""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
+curdir = os.path.abspath(os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy.test import helper, webtest
+
+
+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
+"""
+
+
+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.
+ cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port)
+ 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):
+ import cherrypy
+ 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)
+
diff --git a/cherrypy/test/sessiondemo.py b/cherrypy/test/sessiondemo.py
new file mode 100755
index 0000000..342e5b5
--- /dev/null
+++ b/cherrypy/test/sessiondemo.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+"""A session demonstration app."""
+
+import calendar
+from datetime import datetime
+import sys
+import cherrypy
+from cherrypy.lib import sessions
+from cherrypy._cpcompat import copyitems
+
+
+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'>&nbsp;</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>
+"""
+
+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': copyitems(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,
+ }
+
+ def index(self):
+ # Must modify data or the session will not be saved.
+ cherrypy.session['color'] = 'green'
+ return self.page()
+ index.exposed = True
+
+ def expire(self):
+ sessions.expire()
+ return self.page()
+ expire.exposed = True
+
+ def regen(self):
+ cherrypy.session.regenerate()
+ # Must modify data or the session will not be saved.
+ cherrypy.session['color'] = 'yellow'
+ return self.page()
+ regen.exposed = True
+
+if __name__ == '__main__':
+ cherrypy.config.update({
+ #'environment': 'production',
+ 'log.screen': True,
+ 'tools.sessions.on': True,
+ })
+ cherrypy.quickstart(Root())
+
diff --git a/cherrypy/test/static/dirback.jpg b/cherrypy/test/static/dirback.jpg
new file mode 100644
index 0000000..530e6d6
--- /dev/null
+++ b/cherrypy/test/static/dirback.jpg
Binary files differ
diff --git a/cherrypy/test/static/index.html b/cherrypy/test/static/index.html
new file mode 100644
index 0000000..b9f5f09
--- /dev/null
+++ b/cherrypy/test/static/index.html
@@ -0,0 +1 @@
+Hello, world
diff --git a/cherrypy/test/style.css b/cherrypy/test/style.css
new file mode 100644
index 0000000..b266e93
--- /dev/null
+++ b/cherrypy/test/style.css
@@ -0,0 +1 @@
+Dummy stylesheet
diff --git a/cherrypy/test/test.pem b/cherrypy/test/test.pem
new file mode 100644
index 0000000..47a4704
--- /dev/null
+++ b/cherrypy/test/test.pem
@@ -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-----
diff --git a/cherrypy/test/test_auth_basic.py b/cherrypy/test/test_auth_basic.py
new file mode 100755
index 0000000..3a9781d
--- /dev/null
+++ b/cherrypy/test/test_auth_basic.py
@@ -0,0 +1,79 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+import cherrypy
+from cherrypy._cpcompat import md5, ntob
+from cherrypy.lib import auth_basic
+from cherrypy.test import helper
+
+
+class BasicAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class BasicProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected2:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ userpassdict = {'xuser' : 'xpassword'}
+ userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()}
+
+ def checkpasshash(realm, user, password):
+ p = userhashdict.get(user)
+ return p and p == md5(ntob(password)).hexdigest() or False
+
+ conf = {'/basic': {'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'wonderland',
+ 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)},
+ '/basic2': {'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'wonderland',
+ 'tools.auth_basic.checkpassword': checkpasshash},
+ }
+
+ root = Root()
+ root.basic = BasicProtected()
+ root.basic2 = BasicProtected2()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+ 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"')
+
+ 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.")
+
diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py
new file mode 100755
index 0000000..1960fa8
--- /dev/null
+++ b/cherrypy/test/test_auth_digest.py
@@ -0,0 +1,115 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+
+import cherrypy
+from cherrypy.lib import auth_digest
+
+from cherrypy.test import helper
+
+class DigestAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class DigestProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ def fetch_users():
+ return {'test': 'test'}
+
+
+ get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users())
+ 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'}}
+
+ root = Root()
+ root.digest = DigestProtected()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+ def testPublic(self):
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('This is public.')
+
+ def testDigest(self):
+ self.getPage("/digest/")
+ self.assertStatus(401)
+
+ value = None
+ for k, v in self.headers:
+ if k.lower() == "www-authenticate":
+ if v.startswith("Digest"):
+ value = v
+ break
+
+ if value is None:
+ self._handlewebError("Digest authentification scheme was not found")
+
+ value = value[7:]
+ items = value.split(', ')
+ tokens = {}
+ for item in items:
+ key, value = item.split('=')
+ tokens[key.lower()] = value
+
+ missing_msg = "%s is missing"
+ bad_value_msg = "'%s' was expecting '%s' but found '%s'"
+ nonce = None
+ if 'realm' not in tokens:
+ self._handlewebError(missing_msg % 'realm')
+ elif tokens['realm'] != '"localhost"':
+ self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
+ if 'nonce' not in tokens:
+ self._handlewebError(missing_msg % 'nonce')
+ else:
+ nonce = tokens['nonce'].strip('"')
+ if 'algorithm' not in tokens:
+ self._handlewebError(missing_msg % 'algorithm')
+ elif tokens['algorithm'] != '"MD5"':
+ self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
+ if 'qop' not in tokens:
+ self._handlewebError(missing_msg % 'qop')
+ elif tokens['qop'] != '"auth"':
+ self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
+
+ get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'})
+
+ # Test user agent response with a wrong value for 'realm'
+ base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
+ auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
+ # calculate the response digest
+ ha1 = get_ha1(auth.realm, 'test')
+ response = auth.request_digest(ha1)
+ # send response with correct response digest, but wrong realm
+ auth_header = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth_header)])
+ self.assertStatus(401)
+
+ # Test that must pass
+ base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
+ auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
+ # calculate the response digest
+ ha1 = get_ha1('localhost', 'test')
+ response = auth.request_digest(ha1)
+ # send response with correct response digest
+ auth_header = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth_header)])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py
new file mode 100755
index 0000000..51c1022
--- /dev/null
+++ b/cherrypy/test/test_bus.py
@@ -0,0 +1,263 @@
+import threading
+import time
+import unittest
+
+import cherrypy
+from cherrypy._cpcompat import get_daemon, set
+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 get_daemon(t)]
+ 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 get_daemon(t)]
+ 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()
diff --git a/cherrypy/test/test_caching.py b/cherrypy/test/test_caching.py
new file mode 100755
index 0000000..720a933
--- /dev/null
+++ b/cherrypy/test/test_caching.py
@@ -0,0 +1,329 @@
+import datetime
+import gzip
+from itertools import count
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import sys
+import threading
+import time
+import urllib
+
+import cherrypy
+from cherrypy._cpcompat import next, ntob, quote, xrange
+from cherrypy.lib import httputil
+
+gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
+ '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;')
+
+
+
+from cherrypy.test import helper
+
+class CacheTest(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+
+ _cp_config = {'tools.caching.on': True}
+
+ def __init__(self):
+ self.counter = 0
+ self.control_counter = 0
+ self.longlock = threading.Lock()
+
+ def index(self):
+ self.counter += 1
+ msg = "visit #%s" % self.counter
+ return msg
+ index.exposed = True
+
+ def control(self):
+ self.control_counter += 1
+ return "visit #%s" % self.control_counter
+ control.exposed = True
+
+ def a_gif(self):
+ cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate()
+ return gif_bytes
+ a_gif.exposed = True
+
+ def long_process(self, seconds='1'):
+ try:
+ self.longlock.acquire()
+ time.sleep(float(seconds))
+ finally:
+ self.longlock.release()
+ return 'success!'
+ long_process.exposed = True
+
+ def clear_cache(self, path):
+ cherrypy._cache.store[cherrypy.request.base + path].clear()
+ clear_cache.exposed = True
+
+ class VaryHeaderCachingServer(object):
+
+ _cp_config = {'tools.caching.on': True,
+ 'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')],
+ }
+
+ def __init__(self):
+ self.counter = count(1)
+
+ def index(self):
+ return "visit #%s" % next(self.counter)
+ index.exposed = True
+
+ class UnCached(object):
+ _cp_config = {'tools.expires.on': True,
+ 'tools.expires.secs': 60,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.root': curdir,
+ }
+
+ 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"
+ force.exposed = True
+ force._cp_config = {'tools.expires.secs': 0}
+
+ def dynamic(self):
+ cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+ cherrypy.response.headers['Cache-Control'] = 'private'
+ return "D-d-d-dynamic!"
+ dynamic.exposed = True
+
+ def cacheable(self):
+ cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+ return "Hi, I'm cacheable."
+ cacheable.exposed = True
+
+ def specific(self):
+ cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
+ return "I am being specific"
+ specific.exposed = True
+ specific._cp_config = {'tools.expires.secs': 86400}
+
+ class Foo(object):pass
+
+ def wrongtype(self):
+ cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
+ return "Woops"
+ wrongtype.exposed = True
+ wrongtype._cp_config = {'tools.expires.secs': Foo()}
+
+ cherrypy.tree.mount(Root())
+ cherrypy.tree.mount(UnCached(), "/expires")
+ cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers")
+ cherrypy.config.update({'tools.gzip.on': True})
+ setup_server = staticmethod(setup_server)
+
+ 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), ntob("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), ntob("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 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")
+
+ def test_antistampede(self):
+ SECONDS = 4
+ # 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("/long_process?seconds=%d" % SECONDS)
+ self.assertBody('success!')
+ self.getPage("/clear_cache?path=" +
+ quote('/long_process?seconds=%d' % SECONDS, safe=''))
+ self.assertStatus(200)
+ sys.stdout.write("prepped... ")
+ sys.stdout.flush()
+
+ start = datetime.datetime.now()
+ def run():
+ self.getPage("/long_process?seconds=%d" % SECONDS)
+ # The response should be the same every time
+ self.assertBody('success!')
+ ts = [threading.Thread(target=run) for i in xrange(100)]
+ for t in ts:
+ t.start()
+ for t in ts:
+ t.join()
+ self.assertEqualDates(start, datetime.datetime.now(),
+ # Allow a second for our thread/TCP overhead etc.
+ seconds=SECONDS + 1.1)
+
+ 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')
+
diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py
new file mode 100755
index 0000000..a0bd8ab
--- /dev/null
+++ b/cherrypy/test/test_config.py
@@ -0,0 +1,249 @@
+"""Tests for the CherryPy configuration system."""
+
+import os, sys
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+from cherrypy._cpcompat import ntob, StringIO
+import unittest
+
+import cherrypy
+
+def setup_server():
+
+ class Root:
+
+ _cp_config = {'foo': 'this',
+ 'bar': 'that'}
+
+ 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")
+ index = cherrypy.expose(index, alias=('global_', 'xyz'))
+
+ def repr(self, key):
+ return repr(cherrypy.request.config.get(key, None))
+ repr.exposed = True
+
+ def dbscheme(self):
+ return self.db
+ dbscheme.exposed = True
+
+ def plain(self, x):
+ return x
+ plain.exposed = True
+ plain._cp_config = {'request.body.attempt_charsets': ['utf-16']}
+
+ favicon_ico = cherrypy.tools.staticfile.handler(
+ filename=os.path.join(localDir, '../favicon.ico'))
+
+ class Foo:
+
+ _cp_config = {'foo': 'this2',
+ 'baz': 'that2'}
+
+ def index(self, key):
+ return cherrypy.request.config.get(key, "None")
+ index.exposed = True
+ nex = index
+
+ def silly(self):
+ return 'Hello world'
+ silly.exposed = True
+ silly._cp_config = {'response.headers.X-silly': 'sillyval'}
+
+ def bar(self, key):
+ return repr(cherrypy.request.config.get(key, None))
+ bar.exposed = True
+ bar._cp_config = {'foo': 'this3', 'bax': 'this4'}
+
+ class Another:
+
+ def index(self, key):
+ return str(cherrypy.request.config.get(key, "None"))
+ index.exposed = True
+
+
+ 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
+
+ class Raw:
+
+ _cp_config = {'raw.output': repr}
+
+ def incr(self, num):
+ return num + 1
+ incr.exposed = True
+ incr._cp_config = {'raw.input.map': {'num': int}}
+
+ ioconf = StringIO("""
+[/]
+neg: -1234
+filename: os.path.join(sys.prefix, "hello.py")
+thing1: cherrypy.lib.httputil.response_codes[404]
+thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
+complex: 3+2j
+ones: "11"
+twos: "22"
+stradd: %%(ones)s + %%(twos)s + "33"
+
+[/favicon.ico]
+tools.staticfile.filename = %r
+""" % 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 #
+
+from cherrypy.test import helper
+
+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))
+
+ self.getPage("/repr?key=complex")
+ self.assertBody("(3+2j)")
+
+ 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=ntob('\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 = StringIO(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")
+
diff --git a/cherrypy/test/test_config_server.py b/cherrypy/test/test_config_server.py
new file mode 100755
index 0000000..0b9718d
--- /dev/null
+++ b/cherrypy/test/test_config_server.py
@@ -0,0 +1,121 @@
+"""Tests for the CherryPy configuration system."""
+
+import os, sys
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import socket
+import time
+
+import cherrypy
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class ServerConfigTests(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+ def index(self):
+ return cherrypy.request.wsgi_environ['SERVER_PORT']
+ index.exposed = True
+
+ def upload(self, file):
+ return "Size: %s" % len(file.file.read())
+ upload.exposed = True
+
+ def tinyupload(self):
+ return cherrypy.request.body.read()
+ tinyupload.exposed = True
+ tinyupload._cp_config = {'request.body.maxbytes': 100}
+
+ 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,
+ })
+ setup_server = staticmethod(setup_server)
+
+ 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 http://www.cherrypy.org/ticket/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
+ body = '\r\n'.join([
+ '--x',
+ 'Content-Disposition: form-data; name="file"; filename="hello.txt"',
+ '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)
+
diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py
new file mode 100755
index 0000000..1346f59
--- /dev/null
+++ b/cherrypy/test/test_conn.py
@@ -0,0 +1,734 @@
+"""Tests for TCP connection handling, including proper and timely close."""
+
+import socket
+import sys
+import time
+timeout = 1
+
+
+import cherrypy
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine
+from cherrypy._cpcompat import ntob, urlopen, unicodestr
+from cherrypy.test import webtest
+from cherrypy import _cperror
+
+
+pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
+
+def setup_server():
+
+ def raise500():
+ raise cherrypy.HTTPError(500)
+
+ class Root:
+
+ def index(self):
+ return pov
+ index.exposed = True
+ page1 = index
+ page2 = index
+ page3 = index
+
+ def hello(self):
+ return "Hello, world!"
+ hello.exposed = True
+
+ def timeout(self, t):
+ return str(cherrypy.server.httpserver.timeout)
+ timeout.exposed = 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()
+ stream.exposed = True
+ stream._cp_config = {'response.stream': True}
+
+ def error(self, code=500):
+ raise cherrypy.HTTPError(code)
+ error.exposed = True
+
+ 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()
+ upload.exposed = True
+
+ def custom(self, response_code):
+ cherrypy.response.status = response_code
+ return "Code = %s" % response_code
+ custom.exposed = True
+
+ def err_before_read(self):
+ return "ok"
+ err_before_read.exposed = True
+ err_before_read._cp_config = {'hooks.on_start_resource': raise500}
+
+ def one_megabyte_of_a(self):
+ return ["a" * 1024] * 1024
+ one_megabyte_of_a.exposed = True
+
+ 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, unicodestr):
+ chunk = chunk.encode('ISO-8859-1')
+ newbody.append(chunk)
+ return newbody
+ custom_cl.exposed = True
+ # Turn off the encoding tool so it doens't collapse
+ # our response body and reclaculate the Content-Length.
+ custom_cl._cp_config = {'tools.encode.on': False}
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({
+ 'server.max_request_body_size': 1001,
+ 'server.socket_timeout': timeout,
+ })
+
+
+from cherrypy.test import helper
+
+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):
+ self._streaming(set_cl=False)
+
+ def test_Streaming_with_len(self):
+ self._streaming(set_cl=True)
+
+ 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 http://www.cherrypy.org/ticket/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(ntob('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(ntob('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(ntob('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:
+ 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(ntob('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:
+ 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(ntob('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")
+ response.begin()
+ body = response.read(13)
+ self.assertEqual(response.status, 200)
+ self.assertEqual(body, ntob("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, ntob("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.
+ 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)
+ conn.close()
+
+ # Now try a page with an Expect header...
+ 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 = ntob("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()
+
+
+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(ntob('POST /upload HTTP/1.1'))
+ conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
+ conn._output(ntob("Content-Type: text/plain"))
+ conn._output(ntob("Content-Length: 17"))
+ conn._output(ntob("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 = ntob("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'" % ntob('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):
+ remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' %
+ (self.scheme, self.HOST, self.PORT,))
+ 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()
+
+
+class BadRequestTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_No_CRLF(self):
+ self.persistent = True
+
+ conn = self.HTTP_CONN
+ conn.send(ntob('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(ntob('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()
+
diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py
new file mode 100755
index 0000000..09544e3
--- /dev/null
+++ b/cherrypy/test/test_core.py
@@ -0,0 +1,617 @@
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import types
+
+import cherrypy
+from cherrypy._cpcompat import IncompleteRead, itervalues, ntob
+from cherrypy import _cptools, tools
+from cherrypy.lib import httputil, static
+
+
+favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico")
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class CoreRequestHandlingTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ favicon_ico = tools.staticfile.handler(filename=favicon_path)
+
+ def defct(self, newct):
+ newct = "text/%s" % newct
+ cherrypy.config.update({'tools.response_headers.on': True,
+ 'tools.response_headers.headers':
+ [('Content-Type', newct)]})
+ defct.exposed = True
+
+ def baseurl(self, path_info, relative=None):
+ return cherrypy.url(path_info, relative=bool(relative))
+ baseurl.exposed = True
+
+ root = Root()
+
+ if sys.version_info >= (2, 5):
+ from cherrypy.test._test_decorators import ExposeExamples
+ 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 itervalues(dct):
+ if isinstance(value, types.FunctionType):
+ value.exposed = True
+ setattr(root, name.lower(), cls())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ class URL(Test):
+
+ _cp_config = {'tools.trailing_slash.on': False}
+
+ 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)
+
+
+ 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"
+
+
+ class Redirect(Test):
+
+ class Error:
+ _cp_config = {"tools.err_redirect.on": True,
+ "tools.err_redirect.url": "/errpage",
+ "tools.err_redirect.internal": False,
+ }
+
+ def index(self):
+ raise NameError("redirect_test")
+ index.exposed = True
+ error = Error()
+
+ def index(self):
+ return "child"
+
+ def custom(self, url, code):
+ raise cherrypy.HTTPRedirect(url, code)
+
+ def by_code(self, code):
+ raise cherrypy.HTTPRedirect("somewhere%20else", code)
+ by_code._cp_config = {'tools.trailing_slash.extra': True}
+
+ 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 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("/")
+
+ def choke(self):
+ return 3 / 0
+ choke.exposed = True
+ choke._cp_config = {'hooks.before_error_response': redir_custom}
+
+ 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."
+
+ def early_ir(self, arg):
+ return "whatever"
+ early_ir._cp_config = {'hooks.before_request_body': redir_custom}
+
+
+ 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 ntob("content")
+
+ def as_dblyield(self):
+ yield self.as_yield()
+ as_dblyield._cp_config = {'tools.flatten.on': True}
+
+ 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):
+ for name in names:
+ cookie = cherrypy.request.cookie[name]
+ # Python2's SimpleCookie.__setitem__ won't take unicode keys.
+ cherrypy.response.cookie[str(name)] = cookie.value
+
+
+ cherrypy.tree.mount(root)
+ setup_server = staticmethod(setup_server)
+
+
+ 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 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.assertInBody("<a href='%s/redirect/?id=3'>"
+ "%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.assertInBody("<a href='%s/'>%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.assertInBody("<a href='%s/redirect/by_code?code=307'>"
+ "%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'>\1somewhere%20else</a>")
+ self.assertStatus(300)
+
+ self.getPage("/redirect/by_code?code=301")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(301)
+
+ self.getPage("/redirect/by_code?code=302")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(302)
+
+ self.getPage("/redirect/by_code?code=303")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(303)
+
+ self.getPage("/redirect/by_code?code=307")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%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\/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 http://www.cherrypy.org/ticket/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 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)]")
+
+ # 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 testCookies(self):
+ if sys.version_info >= (2, 5):
+ header_value = lambda x: x
+ else:
+ header_value = lambda x: x+';'
+
+ self.getPage("/cookies/single?name=First",
+ [('Cookie', 'First=Dinsdale;')])
+ self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
+
+ self.getPage("/cookies/multiple?names=First&names=Last",
+ [('Cookie', 'First=Dinsdale; Last=Piranha;'),
+ ])
+ self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
+ self.assertHeader('Set-Cookie', header_value('Last=Piranha'))
+
+ self.getPage("/cookies/single?name=Something-With:Colon",
+ [('Cookie', 'Something-With:Colon=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_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())
+
+ # 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())
+
+ # 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):
+ if not sys.version_info >= (2, 5):
+ return self.skip("skipped (Python 2.5+ only) ")
+
+ # 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")
+
diff --git a/cherrypy/test/test_dynamicobjectmapping.py b/cherrypy/test/test_dynamicobjectmapping.py
new file mode 100755
index 0000000..1e04d08
--- /dev/null
+++ b/cherrypy/test/test_dynamicobjectmapping.py
@@ -0,0 +1,403 @@
+import cherrypy
+from cherrypy._cptree import Application
+from cherrypy.test import helper
+
+script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
+
+
+
+def setup_server():
+ class SubSubRoot:
+ def index(self):
+ return "SubSubRoot index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "SubSubRoot default"
+ default.exposed = True
+
+ def handler(self):
+ return "SubSubRoot handler"
+ handler.exposed = True
+
+ def dispatch(self):
+ return "SubSubRoot dispatch"
+ dispatch.exposed = True
+
+ subsubnodes = {
+ '1': SubSubRoot(),
+ '2': SubSubRoot(),
+ }
+
+ class SubRoot:
+ def index(self):
+ return "SubRoot index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "SubRoot %s" % (args,)
+ default.exposed = True
+
+ def handler(self):
+ return "SubRoot handler"
+ handler.exposed = True
+
+ def _cp_dispatch(self, vpath):
+ return subsubnodes.get(vpath[0], None)
+
+ subnodes = {
+ '1': SubRoot(),
+ '2': SubRoot(),
+ }
+ class Root:
+ def index(self):
+ return "index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "default %s" % (args,)
+ default.exposed = True
+
+ def handler(self):
+ return "handler"
+ handler.exposed = True
+
+ 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 unicode(self.name)
+
+ user_lookup = {
+ 1: User(1, 'foo'),
+ 2: User(2, 'bar'),
+ }
+
+ def make_user(name, id=None):
+ if not id:
+ id = max(*user_lookup.keys()) + 1
+ user_lookup[id] = User(id, name)
+ return id
+
+ class UserContainerNode(object):
+ exposed = True
+
+ def POST(self, name):
+ """
+ Allow the creation of a new Object
+ """
+ return "POST %d" % make_user(name)
+
+ def GET(self):
+ keys = user_lookup.keys()
+ keys.sort()
+ return unicode(keys)
+
+ def dynamic_dispatch(self, vpath):
+ try:
+ id = int(vpath[0])
+ except (ValueError, IndexError):
+ return None
+ return UserInstanceNode(id)
+
+ class UserInstanceNode(object):
+ exposed = True
+ 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 unicode(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:
+ def index(self, a, b):
+ return "custom"
+ index.exposed = True
+
+ def _cp_dispatch(self, vpath):
+ """Make sure that if we don't pop anything from vpath,
+ processing still works.
+ """
+ return self.CustomDispatch()
+
+ def index(self, a, b=None):
+ body = [ 'a:' + str(a) ]
+ if b is not None:
+ body.append(',b:' + str(b))
+ return ''.join(body)
+ index.exposed = True
+
+ def delete(self, a, b):
+ return 'deleting ' + str(a) + ' and ' + str(b)
+ delete.exposed = True
+
+ 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
+
+ def index(self):
+ return "IndexOnly index"
+ index.exposed = True
+
+ class DecoratedPopArgs:
+ """Test _cp_dispatch with @cherrypy.popargs."""
+ def index(self):
+ return "no params"
+ index.exposed = True
+
+ def hi(self):
+ return "hi was not interpreted as 'a' param"
+ hi.exposed = True
+ DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs)
+
+ class NonDecoratedPopArgs:
+ """Test _cp_dispatch = cherrypy.popargs()"""
+
+ _cp_dispatch = cherrypy.popargs('a')
+
+ def index(self, a):
+ return "index: " + str(a)
+ index.exposed = True
+
+ class ParameterizedHandler:
+ """Special handler created for each request"""
+
+ def __init__(self, a):
+ self.a = a
+
+ def index(self):
+ if 'a' in cherrypy.request.params:
+ raise Exception("Parameterized handler argument ended up in request.params")
+ return self.a
+ index.exposed = True
+
+ 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:
+ prefix = 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")
+
diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py
new file mode 100755
index 0000000..67b28ed
--- /dev/null
+++ b/cherrypy/test/test_encoding.py
@@ -0,0 +1,363 @@
+
+import gzip
+import sys
+
+import cherrypy
+from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou
+
+europoundUnicode = ntou('\x80\xa3')
+sing = u"\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?"
+sing8 = sing.encode('utf-8')
+sing16 = sing.encode('utf-16')
+
+
+from cherrypy.test import helper
+
+
+class EncodingTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, param):
+ assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode)
+ yield europoundUnicode
+ index.exposed = True
+
+ def mao_zedong(self):
+ return sing
+ mao_zedong.exposed = True
+
+ def utf8(self):
+ return sing8
+ utf8.exposed = True
+ utf8._cp_config = {'tools.encode.encoding': 'utf-8'}
+
+ 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'
+ return 'Any content'
+ cookies_and_headers.exposed = True
+
+ def reqparams(self, *args, **kwargs):
+ return ntob(', ').join([": ".join((k, v)).encode('utf8')
+ for k, v in cherrypy.request.params.items()])
+ reqparams.exposed = True
+
+ def nontext(self, *args, **kwargs):
+ cherrypy.response.headers['Content-Type'] = 'application/binary'
+ return '\x00\x01\x02\x03'
+ nontext.exposed = True
+ nontext._cp_config = {'tools.encode.text_only': False,
+ 'tools.encode.add_charset': True,
+ }
+
+ class GZIP:
+ def index(self):
+ yield "Hello, world"
+ index.exposed = True
+
+ 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"
+ noshow.exposed = True
+ # Turn encoding off so the gzip tool is the one doing the collapse.
+ noshow._cp_config = {'tools.encode.on': False}
+
+ 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"
+ noshow_stream.exposed = True
+ noshow_stream._cp_config = {'response.stream': True}
+
+ class Decode:
+ def extra_charset(self, *args, **kwargs):
+ return ', '.join([": ".join((k, v))
+ for k, v in cherrypy.request.params.items()])
+ extra_charset.exposed = True
+ extra_charset._cp_config = {
+ 'tools.decode.on': True,
+ 'tools.decode.default_encoding': ['utf-16'],
+ }
+
+ def force_charset(self, *args, **kwargs):
+ return ', '.join([": ".join((k, v))
+ for k, v in cherrypy.request.params.items()])
+ force_charset.exposed = True
+ force_charset._cp_config = {
+ 'tools.decode.on': True,
+ 'tools.decode.encoding': 'utf-16',
+ }
+
+ root = Root()
+ root.gzip = GZIP()
+ root.decode = Decode()
+ cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
+ setup_server = staticmethod(setup_server)
+
+ def test_query_string_decoding(self):
+ europoundUtf8 = europoundUnicode.encode('utf-8')
+ self.getPage(ntob('/?param=') + europoundUtf8)
+ self.assertBody(europoundUtf8)
+
+ # Encoded utf8 query strings MUST be parsed correctly.
+ # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
+ self.getPage("/reqparams?q=%C2%A3")
+ # The return value will be encoded as utf8.
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # Query strings that are incorrectly encoded MUST raise 404.
+ # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX
+ self.getPage("/reqparams?q=%A3")
+ 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=ntob("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 = ntob("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(ntob("q: \xc2\xa3"))
+
+ # ...and in utf16, which is not in the default attempt_charsets list:
+ body = ntob("\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(ntob("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 = ntob("\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 = ntob("\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(ntob("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 = ntob("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(ntob("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 = ntob("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(ntob("text: ab\xe2\x80\x9cc, submit: Create"))
+
+ 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(ntob("text: \xe2\x80\x9c, submit: Create"))
+
+ 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")
+
+ def testGzip(self):
+ zbuf = BytesIO()
+ zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+ zfile.write(ntob("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")
+
+ 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')
+
diff --git a/cherrypy/test/test_etags.py b/cherrypy/test/test_etags.py
new file mode 100755
index 0000000..026f9d6
--- /dev/null
+++ b/cherrypy/test/test_etags.py
@@ -0,0 +1,81 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class ETagTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def resource(self):
+ return "Oh wah ta goo Siam."
+ resource.exposed = True
+
+ def fail(self, code):
+ code = int(code)
+ if 300 <= code <= 399:
+ raise cherrypy.HTTPRedirect([], code)
+ else:
+ raise cherrypy.HTTPError(code)
+ fail.exposed = True
+
+ def unicoded(self):
+ return u'I am a \u1ee4nicode string.'
+ unicoded.exposed = True
+ unicoded._cp_config = {'tools.encode.on': True}
+
+ conf = {'/': {'tools.etags.on': True,
+ 'tools.etags.autotags': True,
+ }}
+ cherrypy.tree.mount(Root(), config=conf)
+ setup_server = staticmethod(setup_server)
+
+ 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)
+
diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py
new file mode 100755
index 0000000..eb72b5b
--- /dev/null
+++ b/cherrypy/test/test_http.py
@@ -0,0 +1,168 @@
+"""Tests for managing HTTP issues (malformed requests, etc)."""
+
+import mimetypes
+
+import cherrypy
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+
+
+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.
+ """
+ BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
+ L = []
+ for key, filename, value in files:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
+ (key, filename))
+ 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
+
+
+
+
+from cherrypy.test import helper
+
+class HTTPTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, *args, **kwargs):
+ return "Hello world!"
+ index.exposed = True
+
+ def no_body(self, *args, **kwargs):
+ return "Hello world!"
+ no_body.exposed = True
+ no_body._cp_config = {'request.process_request_body': False}
+
+ def post_multipart(self, file):
+ """Return a summary ("a * 65536\nb * 65536") of the uploaded file."""
+ contents = file.file.read()
+ summary = []
+ curchar = ""
+ count = 0
+ for c in contents:
+ if c == curchar:
+ count += 1
+ else:
+ if count:
+ summary.append("%s * %d" % (curchar, count))
+ count = 1
+ curchar = c
+ if count:
+ summary.append("%s * %d" % (curchar, count))
+ return ", ".join(summary)
+ post_multipart.exposed = True
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({'server.max_request_body_size': 30000000})
+ setup_server = staticmethod(setup_server)
+
+ 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.
+ if self.scheme == "https":
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.request("POST", "/no_body")
+ response = c.getresponse()
+ self.body = response.fp.read()
+ self.status = str(response.status)
+ self.assertStatus(200)
+ self.assertBody(ntob('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))
+ 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
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ 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)
+ self.assertBody(", ".join(["%s * 65536" % c for c in alphabet]))
+
+ 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
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c._output(ntob('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), ntob("Malformed Request-Line"))
+ c.close()
+
+ def test_malformed_header(self):
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.putrequest('GET', '/')
+ c.putheader('Content-Type', 'text/plain')
+ # See http://www.cherrypy.org/ticket/941
+ c._output(ntob('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.")
+
diff --git a/cherrypy/test/test_httpauth.py b/cherrypy/test/test_httpauth.py
new file mode 100755
index 0000000..9d0eecb
--- /dev/null
+++ b/cherrypy/test/test_httpauth.py
@@ -0,0 +1,151 @@
+import cherrypy
+from cherrypy._cpcompat import md5, sha, ntob
+from cherrypy.lib import httpauth
+
+from cherrypy.test import helper
+
+class HTTPAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class DigestProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected2:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ def fetch_users():
+ return {'test': 'test'}
+
+ def sha_password_encrypter(password):
+ return sha(ntob(password)).hexdigest()
+
+ def fetch_password(username):
+ return sha(ntob('test')).hexdigest()
+
+ conf = {'/digest': {'tools.digest_auth.on': True,
+ 'tools.digest_auth.realm': 'localhost',
+ 'tools.digest_auth.users': fetch_users},
+ '/basic': {'tools.basic_auth.on': True,
+ 'tools.basic_auth.realm': 'localhost',
+ 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}},
+ '/basic2': {'tools.basic_auth.on': True,
+ 'tools.basic_auth.realm': 'localhost',
+ 'tools.basic_auth.users': fetch_password,
+ 'tools.basic_auth.encrypt': sha_password_encrypter}}
+
+ root = Root()
+ root.digest = DigestProtected()
+ root.basic = BasicProtected()
+ root.basic2 = BasicProtected2()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+
+ 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="localhost"')
+
+ self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')])
+ self.assertStatus(401)
+
+ self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
+ def testBasic2(self):
+ self.getPage("/basic2/")
+ self.assertStatus(401)
+ self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"')
+
+ self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')])
+ self.assertStatus(401)
+
+ self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
+ def testDigest(self):
+ self.getPage("/digest/")
+ self.assertStatus(401)
+
+ value = None
+ for k, v in self.headers:
+ if k.lower() == "www-authenticate":
+ if v.startswith("Digest"):
+ value = v
+ break
+
+ if value is None:
+ self._handlewebError("Digest authentification scheme was not found")
+
+ value = value[7:]
+ items = value.split(', ')
+ tokens = {}
+ for item in items:
+ key, value = item.split('=')
+ tokens[key.lower()] = value
+
+ missing_msg = "%s is missing"
+ bad_value_msg = "'%s' was expecting '%s' but found '%s'"
+ nonce = None
+ if 'realm' not in tokens:
+ self._handlewebError(missing_msg % 'realm')
+ elif tokens['realm'] != '"localhost"':
+ self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
+ if 'nonce' not in tokens:
+ self._handlewebError(missing_msg % 'nonce')
+ else:
+ nonce = tokens['nonce'].strip('"')
+ if 'algorithm' not in tokens:
+ self._handlewebError(missing_msg % 'algorithm')
+ elif tokens['algorithm'] != '"MD5"':
+ self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
+ if 'qop' not in tokens:
+ self._handlewebError(missing_msg % 'qop')
+ elif tokens['qop'] != '"auth"':
+ self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
+
+ # Test a wrong 'realm' value
+ base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth = base_auth % (nonce, '', '00000001')
+ params = httpauth.parseAuthorization(auth)
+ response = httpauth._computeDigestResponse(params, 'test')
+
+ auth = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth)])
+ self.assertStatus(401)
+
+ # Test that must pass
+ base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth = base_auth % (nonce, '', '00000001')
+ params = httpauth.parseAuthorization(auth)
+ response = httpauth._computeDigestResponse(params, 'test')
+
+ auth = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth)])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
diff --git a/cherrypy/test/test_httplib.py b/cherrypy/test/test_httplib.py
new file mode 100755
index 0000000..5dc40fd
--- /dev/null
+++ b/cherrypy/test/test_httplib.py
@@ -0,0 +1,29 @@
+"""Tests for cherrypy/lib/httputil.py."""
+
+import unittest
+from cherrypy.lib import httputil
+
+
+class UtilityTests(unittest.TestCase):
+
+ def test_urljoin(self):
+ # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO
+ self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/")
+ self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi")
+ self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/")
+ self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi")
+ self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn", ""), "/sn")
+ self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/")
+ self.assertEqual(httputil.urljoin("/", "/pi"), "/pi")
+ self.assertEqual(httputil.urljoin("/", "/"), "/")
+ self.assertEqual(httputil.urljoin("/", ""), "/")
+ self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/")
+ self.assertEqual(httputil.urljoin("", "/pi"), "/pi")
+ self.assertEqual(httputil.urljoin("", "/"), "/")
+ self.assertEqual(httputil.urljoin("", ""), "/")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cherrypy/test/test_json.py b/cherrypy/test/test_json.py
new file mode 100755
index 0000000..a02c076
--- /dev/null
+++ b/cherrypy/test/test_json.py
@@ -0,0 +1,79 @@
+import cherrypy
+from cherrypy.test import helper
+
+from cherrypy._cpcompat import json
+
+class JsonTest(helper.CPWebCase):
+ def setup_server():
+ class Root(object):
+ def plain(self):
+ return 'hello'
+ plain.exposed = True
+
+ def json_string(self):
+ return 'hello'
+ json_string.exposed = True
+ json_string._cp_config = {'tools.json_out.on': True}
+
+ def json_list(self):
+ return ['a', 'b', 42]
+ json_list.exposed = True
+ json_list._cp_config = {'tools.json_out.on': True}
+
+ def json_dict(self):
+ return {'answer': 42}
+ json_dict.exposed = True
+ json_dict._cp_config = {'tools.json_out.on': True}
+
+ def json_post(self):
+ if cherrypy.request.json == [13, 'c']:
+ return 'ok'
+ else:
+ return 'nok'
+ json_post.exposed = True
+ json_post._cp_config = {'tools.json_in.on': True}
+
+ root = Root()
+ cherrypy.tree.mount(root)
+ setup_server = staticmethod(setup_server)
+
+ 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')
+
diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py
new file mode 100755
index 0000000..5a13cd4
--- /dev/null
+++ b/cherrypy/test/test_logging.py
@@ -0,0 +1,149 @@
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+localDir = os.path.dirname(__file__)
+
+import cherrypy
+
+access_log = os.path.join(localDir, "access.log")
+error_log = os.path.join(localDir, "error.log")
+
+# Some unicode strings.
+tartaros = u'\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2'
+erebos = u'\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com'
+
+
+def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ def uni_code(self):
+ cherrypy.request.login = tartaros
+ cherrypy.request.remote.name = erebos
+ uni_code.exposed = True
+
+ def slashes(self):
+ cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
+ slashes.exposed = True
+
+ 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)'
+ whitespace.exposed = True
+
+ def as_string(self):
+ return "content"
+ as_string.exposed = True
+
+ def as_yield(self):
+ yield "content"
+ as_yield.exposed = True
+
+ def error(self):
+ raise ValueError()
+ error.exposed = True
+ error._cp_config = {'tools.log_tracebacks.on': True}
+
+ root = Root()
+
+
+ cherrypy.config.update({'log.error_file': error_log,
+ 'log.access_file': access_log,
+ })
+ cherrypy.tree.mount(root)
+
+
+
+from cherrypy.test import helper, logtest
+
+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())
+
+ def testEscapedOutput(self):
+ # Test unicode in access log pieces.
+ self.markLog()
+ self.getPage("/uni_code")
+ self.assertStatus(200)
+ 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)
+ 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 Traceback (most recent call last):')
+ self.assertLog(-3, 'raise ValueError()')
+ finally:
+ ignore.pop()
+
diff --git a/cherrypy/test/test_mime.py b/cherrypy/test/test_mime.py
new file mode 100755
index 0000000..605071b
--- /dev/null
+++ b/cherrypy/test/test_mime.py
@@ -0,0 +1,128 @@
+"""Tests for various MIME issues, including the safe_multipart Tool."""
+
+import cherrypy
+from cherrypy._cpcompat import ntob, ntou, sorted
+
+def setup_server():
+
+ class Root:
+
+ def multipart(self, parts):
+ return repr(parts)
+ multipart.exposed = True
+
+ def multipart_form_data(self, **kwargs):
+ return repr(list(sorted(kwargs.items())))
+ multipart_form_data.exposed = True
+
+ def flashupload(self, Filedata, Upload, Filename):
+ return ("Upload: %r, Filename: %r, Filedata: %r" %
+ (Upload, Filename, Filedata.file.read()))
+ flashupload.exposed = True
+
+ cherrypy.config.update({'server.max_request_body_size': 0})
+ cherrypy.tree.mount(Root())
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+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 http://www.cherrypy.org/ticket/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', [u'111', u'333']), ('foo', u'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:8080'),
+ ('Content-Length', '499'),
+ ('Connection', 'Keep-Alive'),
+ ('Cache-Control', 'no-cache'),
+ ]
+ filedata = ntob('<?xml version="1.0" encoding="UTF-8"?>\r\n'
+ '<projectDescription>\r\n'
+ '</projectDescription>\r\n')
+ body = (ntob(
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; name="Filename"\r\n'
+ '\r\n'
+ '.project\r\n'
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; '
+ 'name="Filedata"; filename=".project"\r\n'
+ 'Content-Type: application/octet-stream\r\n'
+ '\r\n')
+ + filedata +
+ ntob('\r\n'
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; name="Upload"\r\n'
+ '\r\n'
+ 'Submit Query\r\n'
+ # Flash apps omit the trailing \r\n on the last line:
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
+ ))
+ self.getPage('/flashupload', headers, "POST", body)
+ self.assertBody("Upload: u'Submit Query', Filename: u'.project', "
+ "Filedata: %r" % filedata)
+
diff --git a/cherrypy/test/test_misc_tools.py b/cherrypy/test/test_misc_tools.py
new file mode 100755
index 0000000..fb94e86
--- /dev/null
+++ b/cherrypy/test/test_misc_tools.py
@@ -0,0 +1,202 @@
+import os
+localDir = os.path.dirname(__file__)
+logfile = os.path.join(localDir, "test_misc_tools.log")
+
+import cherrypy
+from cherrypy import tools
+
+
+def setup_server():
+ class Root:
+ def index(self):
+ yield "Hello, world"
+ index.exposed = True
+ h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')]
+ tools.response_headers(headers=h)(index)
+
+ def other(self):
+ return "salut"
+ other.exposed = True
+ other._cp_config = {
+ 'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [("Content-Language", "fr"),
+ ('Content-Type', 'text/plain')],
+ 'tools.log_hooks.on': True,
+ }
+
+
+ class Accept:
+ _cp_config = {'tools.accept.on': True}
+
+ def index(self):
+ return '<a href="feed">Atom feed</a>'
+ index.exposed = True
+
+ # In Python 2.4+, we could use a decorator instead:
+ # @tools.accept('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>"""
+ feed.exposed = True
+ feed._cp_config = {'tools.accept.media': 'application/atom+xml'}
+
+ 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"
+ select.exposed = True
+
+ class Referer:
+ def accept(self):
+ return "Accepted!"
+ accept.exposed = True
+ reject = accept
+
+ class AutoVary:
+ def index(self):
+ # Read a header directly with 'get'
+ ae = cherrypy.request.headers.get('Accept-Encoding')
+ # Read a header directly with '__getitem__'
+ cl = cherrypy.request.headers['Host']
+ # Read a header directly with '__contains__'
+ hasif = 'If-Modified-Since' in cherrypy.request.headers
+ # Read a header directly with 'has_key'
+ has = cherrypy.request.headers.has_key('Range')
+ # Call a lib function
+ mtype = tools.accept.callable(['text/html', 'text/plain'])
+ return "Hello, world!"
+ index.exposed = True
+
+ 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})
+
+
+from cherrypy.test import helper
+
+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')
+
diff --git a/cherrypy/test/test_objectmapping.py b/cherrypy/test/test_objectmapping.py
new file mode 100755
index 0000000..46816fc
--- /dev/null
+++ b/cherrypy/test/test_objectmapping.py
@@ -0,0 +1,403 @@
+import cherrypy
+from cherrypy._cptree import Application
+from cherrypy.test import helper
+
+script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
+
+
+class ObjectMappingTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, name="world"):
+ return name
+ index.exposed = True
+
+ def foobar(self):
+ return "bar"
+ foobar.exposed = True
+
+ def default(self, *params, **kwargs):
+ return "default:" + repr(params)
+ default.exposed = True
+
+ def other(self):
+ return "other"
+ other.exposed = True
+
+ def extra(self, *p):
+ return repr(p)
+ extra.exposed = True
+
+ def redirect(self):
+ raise cherrypy.HTTPRedirect('dir1/', 302)
+ redirect.exposed = True
+
+ def notExposed(self):
+ return "not exposed"
+
+ def confvalue(self):
+ return cherrypy.request.config.get("user")
+ confvalue.exposed = True
+
+ def redirect_via_url(self, path):
+ raise cherrypy.HTTPRedirect(cherrypy.url(path))
+ redirect_via_url.exposed = True
+
+ def translate_html(self):
+ return "OK"
+ translate_html.exposed = True
+
+ def mapped_func(self, ID=None):
+ return "ID is %s" % ID
+ mapped_func.exposed = True
+ setattr(Root, "Von B\xfclow", mapped_func)
+
+
+ class Exposing:
+ def base(self):
+ return "expose works!"
+ cherrypy.expose(base)
+ cherrypy.expose(base, "1")
+ cherrypy.expose(base, "2")
+
+ class ExposingNewStyle(object):
+ def base(self):
+ return "expose works!"
+ cherrypy.expose(base)
+ cherrypy.expose(base, "1")
+ cherrypy.expose(base, "2")
+
+
+ class Dir1:
+ def index(self):
+ return "index for dir1"
+ index.exposed = True
+
+ def myMethod(self):
+ return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info)
+ myMethod.exposed = True
+ myMethod._cp_config = {'tools.trailing_slash.extra': True}
+
+ def default(self, *params):
+ return "default for dir1, param is:" + repr(params)
+ default.exposed = True
+
+
+ class Dir2:
+ def index(self):
+ return "index for dir2, path is:" + cherrypy.request.path_info
+ index.exposed = True
+
+ def script_name(self):
+ return cherrypy.tree.script_name()
+ script_name.exposed = True
+
+ def cherrypy_url(self):
+ return cherrypy.url("/extra")
+ cherrypy_url.exposed = True
+
+ def posparam(self, *vpath):
+ return "/".join(vpath)
+ posparam.exposed = True
+
+
+ class Dir3:
+ def default(self):
+ return "default for dir3, not exposed"
+
+ class Dir4:
+ def index(self):
+ return "index for dir4, not exposed"
+
+ class DefNoIndex:
+ def default(self, *args):
+ raise cherrypy.HTTPRedirect("contact")
+ default.exposed = True
+
+ # MethodDispatcher code
+ class ByMethod:
+ exposed = True
+
+ 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:
+ def index(self):
+ return "made it!"
+ index.exposed = True
+
+ cherrypy.tree.mount(Isolated(), "/isolated")
+
+ class AnotherApp:
+
+ exposed = True
+
+ def GET(self):
+ return "milk"
+
+ cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}})
+ setup_server = staticmethod(setup_server)
+
+
+ def testObjectMapping(self):
+ for url in script_names:
+ prefix = 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 diferent 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 http://www.cherrypy.org/ticket/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:
+ prefix = 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 http://www.cherrypy.org/ticket/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("['another', u'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):
+ def hello(self):
+ return "Hello world!"
+ hello.exposed = True
+
+ # 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)
+
diff --git a/cherrypy/test/test_proxy.py b/cherrypy/test/test_proxy.py
new file mode 100755
index 0000000..2fbb619
--- /dev/null
+++ b/cherrypy/test/test_proxy.py
@@ -0,0 +1,129 @@
+import cherrypy
+from cherrypy.test import helper
+
+script_names = ["", "/path/to/myapp"]
+
+
+class ProxyTest(helper.CPWebCase):
+
+ 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)
+
+ def pageurl(self):
+ return self.thisnewpage
+ pageurl.exposed = True
+
+ def index(self):
+ raise cherrypy.HTTPRedirect('dummy')
+ index.exposed = True
+
+ def remoteip(self):
+ return cherrypy.request.remote.ip
+ remoteip.exposed = True
+
+ def xhost(self):
+ raise cherrypy.HTTPRedirect('blah')
+ xhost.exposed = True
+ xhost._cp_config = {'tools.proxy.local': 'X-Host',
+ 'tools.trailing_slash.extra': True,
+ }
+
+ def base(self):
+ return cherrypy.request.base
+ base.exposed = True
+
+ def ssl(self):
+ return cherrypy.request.base
+ ssl.exposed = True
+ ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'}
+
+ def newurl(self):
+ return ("Browse to <a href='%s'>this page</a>."
+ % cherrypy.url("/this/new/page"))
+ newurl.exposed = True
+
+ for sn in script_names:
+ cherrypy.tree.mount(Root(sn), sn)
+ setup_server = staticmethod(setup_server)
+
+ 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")
+ self.getPage("/remoteip",
+ headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')])
+ self.assertBody("192.168.0.20")
+
+ # 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 http://www.cherrypy.org/ticket/562).
+ self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')])
+ self.assertHeader('Location', "%s://www.example.test/xhost"
+ % self.scheme)
+
diff --git a/cherrypy/test/test_refleaks.py b/cherrypy/test/test_refleaks.py
new file mode 100755
index 0000000..4df1f08
--- /dev/null
+++ b/cherrypy/test/test_refleaks.py
@@ -0,0 +1,119 @@
+"""Tests for refleaks."""
+
+import gc
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+import threading
+
+import cherrypy
+from cherrypy import _cprequest
+
+
+data = object()
+
+def get_instances(cls):
+ return [x for x in gc.get_objects() if isinstance(x, cls)]
+
+
+from cherrypy.test import helper
+
+
+class ReferenceTests(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+ def index(self, *args, **kwargs):
+ cherrypy.request.thing = data
+ return "Hello world!"
+ index.exposed = True
+
+ def gc_stats(self):
+ output = ["Statistics:"]
+
+ # Uncollectable garbage
+
+ # gc_collect isn't perfectly synchronous, because it may
+ # break reference cycles that then take time to fully
+ # finalize. Call it twice and hope for the best.
+ gc.collect()
+ unreachable = gc.collect()
+ if unreachable:
+ output.append("\n%s unreachable objects:" % unreachable)
+ trash = {}
+ for x in gc.garbage:
+ trash[type(x)] = trash.get(type(x), 0) + 1
+ trash = [(v, k) for k, v in trash.items()]
+ trash.sort()
+ for pair in trash:
+ output.append(" " + repr(pair))
+
+ # Request references
+ reqs = get_instances(_cprequest.Request)
+ lenreqs = len(reqs)
+ if lenreqs < 2:
+ output.append("\nMissing Request reference. Should be 1 in "
+ "this request thread and 1 in the main thread.")
+ elif lenreqs > 2:
+ output.append("\nToo many Request references (%r)." % lenreqs)
+ for req in reqs:
+ output.append("Referrers for %s:" % repr(req))
+ for ref in gc.get_referrers(req):
+ if ref is not reqs:
+ output.append(" %s" % repr(ref))
+
+ # Response references
+ resps = get_instances(_cprequest.Response)
+ lenresps = len(resps)
+ if lenresps < 2:
+ output.append("\nMissing Response reference. Should be 1 in "
+ "this request thread and 1 in the main thread.")
+ elif lenresps > 2:
+ output.append("\nToo many Response references (%r)." % lenresps)
+ for resp in resps:
+ output.append("Referrers for %s:" % repr(resp))
+ for ref in gc.get_referrers(resp):
+ if ref is not resps:
+ output.append(" %s" % repr(ref))
+
+ return "\n".join(output)
+ gc_stats.exposed = True
+
+ cherrypy.tree.mount(Root())
+ setup_server = staticmethod(setup_server)
+
+
+ def test_threadlocal_garbage(self):
+ success = []
+
+ 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, ntob("Hello world!"))
+ finally:
+ c.close()
+ success.append(True)
+
+ ITERATIONS = 25
+ ts = []
+ for _ in range(ITERATIONS):
+ t = threading.Thread(target=getpage)
+ ts.append(t)
+ t.start()
+
+ for t in ts:
+ t.join()
+
+ self.assertEqual(len(success), ITERATIONS)
+
+ self.getPage("/gc_stats")
+ self.assertBody("Statistics:")
+
diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py
new file mode 100755
index 0000000..91ee4fd
--- /dev/null
+++ b/cherrypy/test/test_request_obj.py
@@ -0,0 +1,722 @@
+"""Basic tests for the cherrypy.Request object."""
+
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import types
+from cherrypy._cpcompat import IncompleteRead, ntob, unicodestr
+
+import cherrypy
+from cherrypy import _cptools, tools
+from cherrypy.lib import httputil
+
+defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE",
+ "TRACE", "PROPFIND")
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class RequestObjectTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ def scheme(self):
+ return cherrypy.request.scheme
+ scheme.exposed = True
+
+ 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())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ class Params(Test):
+
+ def index(self, thing):
+ return repr(thing)
+
+ def ismap(self, x, y):
+ return "Coordinates: %s, %s" % (x, y)
+
+ def default(self, *args, **kwargs):
+ return "args: %s kwargs: %s" % (args, kwargs)
+ default._cp_config = {'request.query_string_encoding': 'latin1'}
+
+
+ class ParamErrorsCallable(object):
+ exposed = True
+ def __call__(self):
+ return "data"
+
+ class ParamErrors(Test):
+
+ def one_positional(self, param1):
+ return "data"
+ one_positional.exposed = True
+
+ def one_positional_args(self, param1, *args):
+ return "data"
+ one_positional_args.exposed = True
+
+ def one_positional_args_kwargs(self, param1, *args, **kwargs):
+ return "data"
+ one_positional_args_kwargs.exposed = True
+
+ def one_positional_kwargs(self, param1, **kwargs):
+ return "data"
+ one_positional_kwargs.exposed = True
+
+ def no_positional(self):
+ return "data"
+ no_positional.exposed = True
+
+ def no_positional_args(self, *args):
+ return "data"
+ no_positional_args.exposed = True
+
+ def no_positional_args_kwargs(self, *args, **kwargs):
+ return "data"
+ no_positional_args_kwargs.exposed = True
+
+ def no_positional_kwargs(self, **kwargs):
+ return "data"
+ no_positional_kwargs.exposed = True
+
+ callable_object = ParamErrorsCallable()
+
+ def raise_type_error(self, **kwargs):
+ raise TypeError("Client Error")
+ raise_type_error.exposed = True
+
+ def raise_type_error_with_default_param(self, x, y=None):
+ return '%d' % 'a' # throw an exception
+ raise_type_error_with_default_param.exposed = True
+
+ def callable_error_page(status, **kwargs):
+ return "Error %s - Well, I'm very sorry but you haven't paid!" % status
+
+
+ class Error(Test):
+
+ _cp_config = {'tools.log_tracebacks.on': True,
+ }
+
+ def reason_phrase(self):
+ raise cherrypy.HTTPError("410 Gone fishin'")
+
+ def custom(self, err='404'):
+ raise cherrypy.HTTPError(int(err), "No, <b>really</b>, not found!")
+ custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"),
+ 'error_page.401': callable_error_page,
+ }
+
+ def custom_default(self):
+ return 1 + 'a' # raise an unexpected error
+ custom_default._cp_config = {'error_page.default': callable_error_page}
+
+ def noexist(self):
+ raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
+ noexist._cp_config = {'error_page.404': "nonexistent.html"}
+
+ def page_method(self):
+ raise ValueError()
+
+ def page_yield(self):
+ yield "howdy"
+ raise ValueError()
+
+ def page_streamed(self):
+ yield "word up"
+ raise ValueError()
+ yield "very oops"
+ page_streamed._cp_config = {"response.stream": True}
+
+ def cause_err_in_finalize(self):
+ # Since status must start with an int, this should error.
+ cherrypy.response.status = "ZOO OK"
+ cause_err_in_finalize._cp_config = {'request.show_tracebacks': False}
+
+ def rethrow(self):
+ """Test that an error raised here will be thrown out to the server."""
+ raise ValueError()
+ rethrow._cp_config = {'request.throw_errors': True}
+
+
+ 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 http://www.cherrypy.org/ticket/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, unicodestr)
+ cherrypy.response.headers['ETag'] = val
+ return val
+
+
+ class HeaderElements(Test):
+
+ def get_elements(self, headername):
+ e = cherrypy.request.headers.elements(headername)
+ return "\n".join([unicodestr(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:
+ """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 = {}
+
+ 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>"
+ index.exposed = True
+
+ def get(self, ID):
+ return ("Divorce document %s: %s" %
+ (ID, self.documents.get(ID, "empty")))
+ get.exposed = True
+
+ root.divorce = Divorce()
+
+
+ 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")},
+ }
+ cherrypy.tree.mount(root, config=appconf)
+ setup_server = staticmethod(setup_server)
+
+ def test_scheme(self):
+ self.getPage("/scheme")
+ self.assertBody(self.scheme)
+
+ def testParams(self):
+ self.getPage("/params/?thing=a")
+ self.assertBody("u'a'")
+
+ self.getPage("/params/?thing=a&thing=b&thing=c")
+ self.assertBody("[u'a', u'b', u'c']")
+
+ # Test friendly error message when given params are not accepted.
+ cherrypy.config.update({"request.show_mismatched_params": True})
+ self.getPage("/params/?notathing=meeting")
+ self.assertInBody("Missing parameters: thing")
+ self.getPage("/params/?thing=meeting&notathing=meeting")
+ self.assertInBody("Unexpected query string parameters: notathing")
+
+ # Test ability to turn off friendly error messages
+ cherrypy.config.update({"request.show_mismatched_params": False})
+ self.getPage("/params/?notathing=meeting")
+ self.assertInBody("Not Found")
+ self.getPage("/params/?thing=meeting&notathing=meeting")
+ self.assertInBody("Not Found")
+
+ # Test "% HEX HEX"-encoded URL, param keys, and values
+ self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
+ self.assertBody(r"args: ('\xd4 \xe3', 'cheese') "
+ r"kwargs: {'Gruy\xe8re': u'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(r"args: ('code',) "
+ r"kwargs: {'url': u'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: ('dictlike',) "
+ "kwargs: {'a[1]': u'1', 'b[bar]': u'baz', 'b': u'foo', 'a[2]': u'2'}")
+
+ def testParamErrors(self):
+
+ # test that all of the handlers work when given
+ # the correct parameters in order to ensure that the
+ # errors below aren't coming from some other source.
+ for uri in (
+ '/paramerrors/one_positional?param1=foo',
+ '/paramerrors/one_positional_args?param1=foo',
+ '/paramerrors/one_positional_args/foo',
+ '/paramerrors/one_positional_args/foo/bar/baz',
+ '/paramerrors/one_positional_args_kwargs?param1=foo&param2=bar',
+ '/paramerrors/one_positional_args_kwargs/foo?param2=bar&param3=baz',
+ '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+ '/paramerrors/one_positional_kwargs?param1=foo&param2=bar&param3=baz',
+ '/paramerrors/one_positional_kwargs/foo?param4=foo&param2=bar&param3=baz',
+ '/paramerrors/no_positional',
+ '/paramerrors/no_positional_args/foo',
+ '/paramerrors/no_positional_args/foo/bar/baz',
+ '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
+ '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
+ '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+ '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
+ '/paramerrors/callable_object',
+ ):
+ self.getPage(uri)
+ self.assertStatus(200)
+
+ # query string parameters are part of the URI, so if they are wrong
+ # for a particular handler, the status MUST be a 404.
+ error_msgs = [
+ 'Missing parameters',
+ 'Nothing matches the given URI',
+ 'Multiple values for parameters',
+ 'Unexpected query string parameters',
+ 'Unexpected body parameters',
+ ]
+ for uri, msg in (
+ ('/paramerrors/one_positional', error_msgs[0]),
+ ('/paramerrors/one_positional?foo=foo', error_msgs[0]),
+ ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
+ ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
+ ('/paramerrors/one_positional/foo?param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]),
+ ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/one_positional_kwargs/foo?param1=foo&param2=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/no_positional/boo', error_msgs[1]),
+ ('/paramerrors/no_positional?param1=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]),
+ ('/paramerrors/callable_object?param1=foo', error_msgs[3]),
+ ('/paramerrors/callable_object/boo', error_msgs[1]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri)
+ self.assertStatus(404)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Not Found")
+
+ # if body parameters are wrong, a 400 must be returned.
+ for uri, body, msg in (
+ ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]),
+ ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]),
+ ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
+ ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]),
+ ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri, method='POST', body=body)
+ self.assertStatus(400)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Bad Request")
+
+
+ # even if body parameters are wrong, if we get the uri wrong, then
+ # it's a 404
+ for uri, body, msg in (
+ ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]),
+ ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]),
+ ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]),
+ ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz', error_msgs[1]),
+ ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]),
+ ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri, method='POST', body=body)
+ self.assertStatus(404)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Not Found")
+
+ # In the case that a handler raises a TypeError we should
+ # let that type error through.
+ for uri in (
+ '/paramerrors/raise_type_error',
+ '/paramerrors/raise_type_error_with_default_param?x=0',
+ '/paramerrors/raise_type_error_with_default_param?x=0&y=0',
+ ):
+ 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)
+ msg = ("No, &lt;b&gt;really&lt;/b&gt;, not found!<br />"
+ "In addition, the custom error page failed:\n<br />"
+ "IOError: [Errno 2] No such file or directory: 'nonexistent.html'")
+ 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 http://www.cherrypy.org/ticket/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 http://www.cherrypy.org/ticket/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(u"=?utf-8?q?f=C3=BCr?="), u"f\xfcr")
+
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ # Test RFC-2047-encoded request and response header values
+ u = u'\u212bngstr\xf6m'
+ c = u"=E2=84=ABngstr=C3=B6m"
+ self.getPage("/headers/ifmatch", [('If-Match', u'=?utf-8?q?%s?=' % c)])
+ # The body should be utf-8 encoded.
+ self.assertBody("\xe2\x84\xabngstr\xc3\xb6m")
+ # But the Etag header should be RFC-2047 encoded (binary)
+ self.assertHeader("ETag", u'=?utf-8?b?4oSrbmdzdHLDtm0=?=')
+
+ # Test a *LONG* RFC-2047-encoded request and response header value
+ self.getPage("/headers/ifmatch",
+ [('If-Match', u'=?utf-8?q?%s?=' % (c * 10))])
+ self.assertBody("\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")
+
+ # 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], ntob("TRACE"))
+ else:
+ self.assertBody(m)
+
+ # 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 http://www.cherrypy.org/ticket/790.
+ b = ntob("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 http://www.cherrypy.org/ticket/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):
+ if getattr(cherrypy.server, "using_apache", False):
+ return self.skip("skipped due to known Apache differences... ")
+
+ self.getPage("/method/", method="CONNECT")
+ self.assertBody("CONNECT")
+
+ def testEmptyThreadlocals(self):
+ results = []
+ for x in range(20):
+ self.getPage("/threadlocal/")
+ results.append(self.body)
+ self.assertEqual(results, [ntob("None")] * 20)
+
diff --git a/cherrypy/test/test_routes.py b/cherrypy/test/test_routes.py
new file mode 100755
index 0000000..a8062f8
--- /dev/null
+++ b/cherrypy/test/test_routes.py
@@ -0,0 +1,69 @@
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import cherrypy
+
+from cherrypy.test import helper
+import nose
+
+class RoutesDispatchTest(helper.CPWebCase):
+
+ def setup_server():
+
+ try:
+ import routes
+ except ImportError:
+ raise nose.SkipTest('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
+
+ def index(self, **kwargs):
+ return "Welcome to %s, pop. %s" % (self.name, self.population)
+ index._cp_config = {'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [('Content-Language', 'en-GB')]}
+
+ 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)
+ setup_server = staticmethod(setup_server)
+
+ def test_Routes_Dispatch(self):
+ 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")
+
diff --git a/cherrypy/test/test_session.py b/cherrypy/test/test_session.py
new file mode 100755
index 0000000..874023e
--- /dev/null
+++ b/cherrypy/test/test_session.py
@@ -0,0 +1,464 @@
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import threading
+import time
+
+import cherrypy
+from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection
+from cherrypy.lib import sessions
+from cherrypy.lib.httputil import response_codes
+
+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():
+
+ class Root:
+
+ _cp_config = {'tools.sessions.on': True,
+ 'tools.sessions.storage_type' : 'ram',
+ 'tools.sessions.storage_path' : localDir,
+ 'tools.sessions.timeout': (1.0 / 60),
+ 'tools.sessions.clean_freq': (1.0 / 60),
+ }
+
+ def clear(self):
+ cherrypy.session.cache.clear()
+ clear.exposed = True
+
+ def data(self):
+ cherrypy.session['aha'] = 'foo'
+ return repr(cherrypy.session._data)
+ data.exposed = True
+
+ def testGen(self):
+ counter = cherrypy.session.get('counter', 0) + 1
+ cherrypy.session['counter'] = counter
+ yield str(counter)
+ testGen.exposed = True
+
+ def testStr(self):
+ counter = cherrypy.session.get('counter', 0) + 1
+ cherrypy.session['counter'] = counter
+ return str(counter)
+ testStr.exposed = True
+
+ def setsessiontype(self, newtype):
+ self.__class__._cp_config.update({'tools.sessions.storage_type': newtype})
+ if hasattr(cherrypy, "session"):
+ del cherrypy.session
+ cls = getattr(sessions, newtype.title() + 'Session')
+ if cls.clean_thread:
+ cls.clean_thread.stop()
+ cls.clean_thread.unsubscribe()
+ del cls.clean_thread
+ setsessiontype.exposed = True
+ setsessiontype._cp_config = {'tools.sessions.on': False}
+
+ def index(self):
+ sess = cherrypy.session
+ c = sess.get('counter', 0) + 1
+ time.sleep(0.01)
+ sess['counter'] = c
+ return str(c)
+ index.exposed = True
+
+ def keyin(self, key):
+ return str(key in cherrypy.session)
+ keyin.exposed = True
+
+ def delete(self):
+ cherrypy.session.delete()
+ sessions.expire()
+ return "done"
+ delete.exposed = True
+
+ def delkey(self, key):
+ del cherrypy.session[key]
+ return "OK"
+ delkey.exposed = True
+
+ def blah(self):
+ return self._cp_config['tools.sessions.storage_type']
+ blah.exposed = True
+
+ def iredir(self):
+ raise cherrypy.InternalRedirect('/blah')
+ iredir.exposed = True
+
+ def restricted(self):
+ return cherrypy.request.method
+ restricted.exposed = True
+ restricted._cp_config = {'tools.allow.on': True,
+ 'tools.allow.methods': ['GET']}
+
+ def regen(self):
+ cherrypy.tools.sessions.regenerate()
+ return "logged in"
+ regen.exposed = True
+
+ def length(self):
+ return str(len(cherrypy.session))
+ length.exposed = True
+
+ def session_cookie(self):
+ # Must load() to start the clean thread.
+ cherrypy.session.load()
+ return cherrypy.session.id
+ session_cookie.exposed = True
+ session_cookie._cp_config = {
+ 'tools.sessions.path': '/session_cookie',
+ 'tools.sessions.name': 'temp',
+ 'tools.sessions.persistent': False}
+
+ cherrypy.tree.mount(Root())
+
+
+from cherrypy.test import helper
+
+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):
+ os.unlink(os.path.join(localDir, fname))
+
+ def test_0_Session(self):
+ self.getPage('/setsessiontype/ram')
+ 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.assertBody("{'aha': 'foo', 'counter': 3}")
+ self.getPage('/length', self.cookies)
+ self.assertBody('2')
+ self.getPage('/delkey?key=counter', self.cookies)
+ self.assertStatus(200)
+
+ self.getPage('/setsessiontype/file')
+ 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")
+ f = lambda: [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('/')
+ f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
+ self.assertNotEqual(f(), [])
+ time.sleep(2)
+ self.assertEqual(f(), [])
+
+ def test_1_Ram_Concurrency(self):
+ self.getPage('/setsessiontype/ram')
+ self._test_Concurrency()
+
+ def test_2_File_Concurrency(self):
+ self.getPage('/setsessiontype/file')
+ 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("file")
+
+ 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])
+
+ 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('/setsessiontype/ram')
+ 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(copykeys(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(copykeys(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 = copykeys(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)
+
+
+import socket
+try:
+ import 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('/setsessiontype/memcached')
+
+ 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] = v = 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])
+
diff --git a/cherrypy/test/test_sessionauthenticate.py b/cherrypy/test/test_sessionauthenticate.py
new file mode 100755
index 0000000..ab1fe51
--- /dev/null
+++ b/cherrypy/test/test_sessionauthenticate.py
@@ -0,0 +1,62 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class SessionAuthenticateTest(helper.CPWebCase):
+
+ 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,
+ }
+
+ def index(self, **kwargs):
+ return "Hi %s, you are logged in" % cherrypy.request.login
+ index.exposed = True
+
+ cherrypy.tree.mount(Test())
+ setup_server = staticmethod(setup_server)
+
+
+ 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">')
+
diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py
new file mode 100755
index 0000000..0f97337
--- /dev/null
+++ b/cherrypy/test/test_states.py
@@ -0,0 +1,436 @@
+from cherrypy._cpcompat import BadStatusLine, ntob
+import os
+import sys
+import threading
+import time
+
+import cherrypy
+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:
+ def index(self):
+ return "Hello World"
+ index.exposed = True
+
+ def ctrlc(self):
+ raise KeyboardInterrupt()
+ ctrlc.exposed = True
+
+ def graceful(self):
+ engine.graceful()
+ return "app was (gracefully) restarted succesfully"
+ graceful.exposed = True
+
+ def block_explicit(self):
+ while True:
+ if cherrypy.response.timed_out:
+ cherrypy.response.timed_out = False
+ return "broken!"
+ time.sleep(0.01)
+ block_explicit.exposed = True
+
+ def block_implicit(self):
+ time.sleep(0.5)
+ return "response.timeout = %s" % cherrypy.response.timeout
+ block_implicit.exposed = True
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({
+ 'environment': 'test_suite',
+ 'engine.deadlock_poll_freq': 0.1,
+ })
+
+ db_connection.subscribe()
+
+
+
+# ------------ Enough helpers. Time for real live test cases. ------------ #
+
+
+from cherrypy.test import helper
+
+class ServerStateTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def setUp(self):
+ cherrypy.server.socket_timeout = 0.1
+
+ 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
+ self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port)
+
+ # 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()
+
+ try:
+ self.getPage("/ctrlc")
+ 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)
+
+ def test_3_Deadlocks(self):
+ cherrypy.config.update({'response.timeout': 0.2})
+
+ engine.start()
+ cherrypy.server.start()
+ try:
+ self.assertNotEqual(engine.timeout_monitor.thread, None)
+
+ # Request a "normal" page.
+ self.assertEqual(engine.timeout_monitor.servings, [])
+ self.getPage("/")
+ self.assertBody("Hello World")
+ # request.close is called async.
+ while engine.timeout_monitor.servings:
+ sys.stdout.write(".")
+ time.sleep(0.01)
+
+ # Request a page that explicitly checks itself for deadlock.
+ # The deadlock_timeout should be 2 secs.
+ self.getPage("/block_explicit")
+ self.assertBody("broken!")
+
+ # Request a page that implicitly breaks deadlock.
+ # If we deadlock, we want to touch as little code as possible,
+ # so we won't even call handle_error, just bail ASAP.
+ self.getPage("/block_implicit")
+ self.assertStatus(500)
+ self.assertInBody("raise cherrypy.TimeoutError()")
+ finally:
+ engine.exit()
+
+ def test_4_Autoreload(self):
+ # 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
+ cherrypy._cpserver.wait_for_occupied_port(host, port)
+
+ self.getPage("/start")
+ self.assert_(float(self.body) > start)
+ finally:
+ # Shut down the spawned process
+ self.getPage("/exit")
+ p.join()
+
+ def test_5_Start_Error(self):
+ # 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 test_SIGTERM(self):
+ # SIGTERM should shut down the server whether daemonized or not.
+ try:
+ from signal import SIGTERM
+ except ImportError:
+ return self.skip("skipped (no SIGTERM) ")
+
+ try:
+ from os import kill
+ except ImportError:
+ return self.skip("skipped (no os.kill) ")
+
+ # 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(), 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(), SIGTERM)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ def test_signal_handler_unsubscribe(self):
+ try:
+ from signal import SIGTERM
+ except ImportError:
+ return self.skip("skipped (no SIGTERM) ")
+
+ try:
+ from os import kill
+ except ImportError:
+ return self.skip("skipped (no os.kill) ")
+
+ # 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')
+ # Send a SIGTERM
+ os.kill(p.get_pid(), SIGTERM)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ # Assert the old handler ran.
+ target_line = open(p.error_log, 'rb').readlines()[-10]
+ if not ntob("I am an old SIGTERM handler.") in target_line:
+ self.fail("Old SIGTERM handler did not run.\n%r" % target_line)
+
diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py
new file mode 100755
index 0000000..871420b
--- /dev/null
+++ b/cherrypy/test/test_static.py
@@ -0,0 +1,300 @@
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+from cherrypy._cpcompat import BytesIO
+
+import os
+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")
+BIGFILE_SIZE = 1024 * 1024
+import threading
+
+import cherrypy
+from cherrypy.lib import static
+from cherrypy.test import helper
+
+
+class StaticTest(helper.CPWebCase):
+
+ def setup_server():
+ if not os.path.exists(has_space_filepath):
+ open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n'))
+ if not os.path.exists(bigfile_filepath):
+ open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE))
+
+ class Root:
+
+ def bigfile(self):
+ from cherrypy.lib import static
+ self.f = static.serve_file(bigfile_filepath)
+ return self.f
+ bigfile.exposed = True
+ bigfile._cp_config = {'response.stream': True}
+
+ def tell(self):
+ if self.f.input.closed:
+ return ''
+ return repr(self.f.input.tell()).rstrip('L')
+ tell.exposed = True
+
+ def fileobj(self):
+ f = open(os.path.join(curdir, 'style.css'), 'rb')
+ return static.serve_fileobj(f, content_type='text/css')
+ fileobj.exposed = True
+
+ def bytesio(self):
+ f = BytesIO(ntob('Fee\nfie\nfo\nfum'))
+ return static.serve_fileobj(f, content_type='text/plain')
+ bytesio.exposed = True
+
+ class Static:
+
+ def index(self):
+ return 'You want the Baron? You can have the Baron!'
+ index.exposed = True
+
+ def dynamic(self):
+ return "This is a DYNAMIC page"
+ dynamic.exposed = True
+
+
+ root = Root()
+ root.static = Static()
+
+ rootconf = {
+ '/static': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.root': 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,
+ },
+ }
+ 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)
+ setup_server = staticmethod(setup_server)
+
+
+ def teardown_server():
+ for f in (has_space_filepath, bigfile_filepath):
+ if os.path.exists(f):
+ try:
+ os.unlink(f)
+ except:
+ pass
+ teardown_server = staticmethod(teardown_server)
+
+
+ def testStatic(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')
+
+ 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/'>"
+ "%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)
+ self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 "
+ "(positional )?arguments \(0 given\)"))
+
+ 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')
+
+ 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 = ntob('')
+ 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(
+ ntob("/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)
+
+ expected = len(body)
+ if tell_position >= BIGFILE_SIZE:
+ # We can't exactly control how much content the server asks for.
+ # Fudge it by only checking the first half of the reads.
+ if expected < (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)" %
+ expected)
+ elif tell_position < expected:
+ 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 (65536)" %
+ (expected, tell_position))
+
+ if body != ntob("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 != ntob("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 != ntob("x" * BIGFILE_SIZE):
+ self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+ (BIGFILE_SIZE, self.body[:50], len(body)))
+
diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py
new file mode 100755
index 0000000..bc8579f
--- /dev/null
+++ b/cherrypy/test/test_tools.py
@@ -0,0 +1,393 @@
+"""Test the various means of instantiating and invoking tools."""
+
+import gzip
+import sys
+from cherrypy._cpcompat import BytesIO, copyitems, itervalues, IncompleteRead, ntob, ntou, xrange
+import time
+timeout = 0.2
+import types
+
+import cherrypy
+from cherrypy import tools
+
+
+europoundUnicode = ntou('\x80\xa3')
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+
+class ToolTests(helper.CPWebCase):
+ 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 = copyitems(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(ntob("good"), ntob("horrorshow"))
+ chunk = chunk.replace(ntob("piece"), ntob("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 = [ntob("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()
+ 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):
+ cherrypy.response.output = o = BytesIO()
+ try:
+ response = 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:
+ def index(self):
+ return "Howdy earth!"
+ index.exposed = True
+
+ def tarfile(self):
+ cherrypy.response.output.write(ntob('I am '))
+ cherrypy.response.output.write(ntob('a tarfile'))
+ tarfile.exposed = True
+ tarfile._cp_config = {'tools.streamer.on': True}
+
+ 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
+ euro.exposed = True
+
+ # Bare hooks
+ def pipe(self):
+ return cherrypy.request.body
+ pipe.exposed = True
+ pipe._cp_config = {'hooks.before_request_body': pipe_body}
+
+ # Multiple decorators; include kwargs just for fun.
+ # Note that rotator must run before gzip.
+ def decorated_euro(self, *vpath):
+ yield ntou("Hello,")
+ yield ntou("world")
+ yield europoundUnicode
+ decorated_euro.exposed = True
+ 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 itervalues(dct):
+ if isinstance(value, types.FunctionType):
+ value.exposed = True
+ setattr(root, name.lower(), cls())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ # METHOD ONE:
+ # Declare Tools in _cp_config
+ class Demo(Test):
+
+ _cp_config = {"tools.nadsat.on": True}
+
+ 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!"
+
+ def stream(self, id=None):
+ for x in xrange(100000000):
+ yield str(x)
+ stream._cp_config = {'response.stream': True}
+
+
+ conf = {
+ # METHOD THREE:
+ # Declare Tools in detached config
+ '/demo': {
+ 'tools.numerify.on': True,
+ 'tools.numerify.map': {ntob("pie"): ntob("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
+
+ if sys.version_info >= (2, 5):
+ from cherrypy.test import _test_decorators
+ root.tooldecs = _test_decorators.ToolExamples()
+ setup_server = staticmethod(setup_server)
+
+ 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)
+ self.assertInBody("AttributeError: 'str' object has no attribute 'items'")
+
+ def testCombinedTools(self):
+ expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8')
+ zbuf = 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 = 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")])
+ 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:
+ numon = 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.")
+
diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py
new file mode 100755
index 0000000..aab2786
--- /dev/null
+++ b/cherrypy/test/test_tutorials.py
@@ -0,0 +1,201 @@
+import sys
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class TutorialTest(helper.CPWebCase):
+
+ def setup_server(cls):
+
+ conf = cherrypy.config.copy()
+
+ def load_tut_module(name):
+ """Import or reload tutorial module as needed."""
+ cherrypy.config.reset()
+ cherrypy.config.update(conf)
+
+ target = "cherrypy.tutorial." + name
+ if target in sys.modules:
+ module = reload(sys.modules[target])
+ else:
+ module = __import__(target, globals(), locals(), [''])
+ # The above import will probably mount a new app at "".
+ app = cherrypy.tree.apps[""]
+
+ app.root.load_tut_module = load_tut_module
+ app.root.sessions = sessions
+ app.root.traceback_setting = traceback_setting
+
+ cls.supervisor.sync_apps()
+ load_tut_module.exposed = True
+
+ def sessions():
+ cherrypy.config.update({"tools.sessions.on": True})
+ sessions.exposed = True
+
+ def traceback_setting():
+ return repr(cherrypy.request.show_tracebacks)
+ traceback_setting.exposed = True
+
+ class Dummy:
+ pass
+ root = Dummy()
+ root.load_tut_module = load_tut_module
+ cherrypy.tree.mount(root)
+ setup_server = classmethod(setup_server)
+
+
+ def test01HelloWorld(self):
+ self.getPage("/load_tut_module/tut01_helloworld")
+ self.getPage("/")
+ self.assertBody('Hello world!')
+
+ def test02ExposeMethods(self):
+ self.getPage("/load_tut_module/tut02_expose_methods")
+ self.getPage("/showMessage")
+ self.assertBody('Hello world!')
+
+ def test03GetAndPost(self):
+ self.getPage("/load_tut_module/tut03_get_and_post")
+
+ # 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.getPage("/load_tut_module/tut04_complex_site")
+ 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.mornography.de">Hendrik's weblog</a></li>
+ </ul>
+
+ <p>[<a href="../">Return to links page</a>]</p>'''
+ self.getPage("/links/extra/")
+ self.assertBody(msg)
+
+ def test05DerivedObjects(self):
+ self.getPage("/load_tut_module/tut05_derived_objects")
+ msg = '''
+ <html>
+ <head>
+ <title>Another Page</title>
+ <head>
+ <body>
+ <h2>Another Page</h2>
+
+ <p>
+ And this is the amazing second page!
+ </p>
+
+ </body>
+ </html>
+ '''
+ self.getPage("/another/")
+ self.assertBody(msg)
+
+ def test06DefaultMethod(self):
+ self.getPage("/load_tut_module/tut06_default_method")
+ self.getPage('/hendrik')
+ self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
+ '(<a href="./">back</a>)')
+
+ def test07Sessions(self):
+ self.getPage("/load_tut_module/tut07_sessions")
+ self.getPage("/sessions")
+
+ 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.getPage("/load_tut_module/tut08_generators_and_yield")
+ 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.getPage("/load_tut_module/tut09_files")
+
+ # 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' + \
+ '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.getPage("/load_tut_module/tut10_http_errors")
+
+ 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'")
+
diff --git a/cherrypy/test/test_virtualhost.py b/cherrypy/test/test_virtualhost.py
new file mode 100755
index 0000000..d6eed0e
--- /dev/null
+++ b/cherrypy/test/test_virtualhost.py
@@ -0,0 +1,107 @@
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class VirtualHostTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "Hello, world"
+ index.exposed = True
+
+ def dom4(self):
+ return "Under construction"
+ dom4.exposed = True
+
+ def method(self, value):
+ return "You sent %s" % repr(value)
+ method.exposed = True
+
+ class VHost:
+ def __init__(self, sitename):
+ self.sitename = sitename
+
+ def index(self):
+ return "Welcome to %s" % self.sitename
+ index.exposed = True
+
+ def vmethod(self, value):
+ return "You sent %s" % repr(value)
+ vmethod.exposed = True
+
+ def url(self):
+ return cherrypy.url("nextpage")
+ url.exposed = True
+
+ # 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',
+ },
+ })
+ setup_server = staticmethod(setup_server)
+
+ 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 u'root'")
+ self.getPage("/vmethod?value=dom2+GET", [('Host', 'www.mydom2.com')])
+ self.assertBody("You sent u'dom2 GET'")
+ self.getPage("/vmethod", [('Host', 'www.mydom3.com')], method="POST",
+ body="value=dom3+POST")
+ self.assertBody("You sent u'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.assertHeader('Content-Type', 'image/jpeg')
+
+ # 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)
+
diff --git a/cherrypy/test/test_wsgi_ns.py b/cherrypy/test/test_wsgi_ns.py
new file mode 100755
index 0000000..d57013c
--- /dev/null
+++ b/cherrypy/test/test_wsgi_ns.py
@@ -0,0 +1,80 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_Namespace_Test(helper.CPWebCase):
+
+ 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 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)()
+ 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
+ return ReplaceResults(res)
+
+ class Root(object):
+
+ def index(self):
+ return "HellO WoRlD!"
+ index.exposed = True
+
+
+ root_conf = {'wsgi.pipeline': [('replace', Replacer)],
+ 'wsgi.replace.map': {'L': 'X', 'l': 'r'},
+ }
+
+ app = cherrypy.Application(Root())
+ app.wsgiapp.pipeline.append(('changecase', ChangeCase))
+ app.wsgiapp.config['changecase'] = {'to': 'upper'}
+ cherrypy.tree.mount(app, config={'/': root_conf})
+ setup_server = staticmethod(setup_server)
+
+
+ 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!")
+
diff --git a/cherrypy/test/test_wsgi_vhost.py b/cherrypy/test/test_wsgi_vhost.py
new file mode 100755
index 0000000..abb1a91
--- /dev/null
+++ b/cherrypy/test/test_wsgi_vhost.py
@@ -0,0 +1,36 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_VirtualHost_Test(helper.CPWebCase):
+
+ def setup_server():
+
+ class ClassOfRoot(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def index(self):
+ return "Welcome to the %s website!" % self.name
+ index.exposed = True
+
+
+ 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))
+ setup_server = staticmethod(setup_server)
+
+ 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)
+
diff --git a/cherrypy/test/test_wsgiapps.py b/cherrypy/test/test_wsgiapps.py
new file mode 100755
index 0000000..fa5420c
--- /dev/null
+++ b/cherrypy/test/test_wsgiapps.py
@@ -0,0 +1,111 @@
+from cherrypy.test import helper
+
+
+class WSGIGraftTests(helper.CPWebCase):
+
+ def setup_server():
+ import os
+ curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+ import cherrypy
+
+ 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 output
+
+ def test_empty_string_app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ return ['Hello', '', ' ', '', 'world']
+
+
+ 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 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):
+ def next(this):
+ line = list(this.iter.next())
+ line.reverse()
+ return "".join(line)
+ return Reverser(results)
+
+ class Root:
+ def index(self):
+ return "I'm a regular CherryPy page handler!"
+ index.exposed = True
+
+
+ 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')
+ setup_server = staticmethod(setup_server)
+
+ 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):
+ import cherrypy
+ 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):
+ import cherrypy
+ 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):
+ import cherrypy
+ 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')
+
diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py
new file mode 100755
index 0000000..c4bf61e
--- /dev/null
+++ b/cherrypy/test/test_xmlrpc.py
@@ -0,0 +1,172 @@
+import sys
+from xmlrpclib import DateTime, Fault, ServerProxy, SafeTransport
+
+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 xmlrpclib.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())
+
+import cherrypy
+
+
+def setup_server():
+ from cherrypy import _cptools
+
+ class Root:
+ def index(self):
+ return "I'm a standard index!"
+ index.exposed = True
+
+
+ class XmlRpc(_cptools.XMLRPCController):
+
+ def foo(self):
+ return "Hello world!"
+ foo.exposed = True
+
+ def return_single_item_list(self):
+ return [42]
+ return_single_item_list.exposed = True
+
+ def return_string(self):
+ return "here is a string"
+ return_string.exposed = True
+
+ def return_tuple(self):
+ return ('here', 'is', 1, 'tuple')
+ return_tuple.exposed = True
+
+ def return_dict(self):
+ return dict(a=1, b=2, c=3)
+ return_dict.exposed = True
+
+ def return_composite(self):
+ return dict(a=1,z=26), 'hi', ['welcome', 'friend']
+ return_composite.exposed = True
+
+ def return_int(self):
+ return 42
+ return_int.exposed = True
+
+ def return_float(self):
+ return 3.14
+ return_float.exposed = True
+
+ def return_datetime(self):
+ return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
+ return_datetime.exposed = True
+
+ def return_boolean(self):
+ return True
+ return_boolean.exposed = True
+
+ def test_argument_passing(self, num):
+ return num * 2
+ test_argument_passing.exposed = True
+
+ def test_returning_Fault(self):
+ return Fault(1, "custom Fault response")
+ test_returning_Fault.exposed = True
+
+ root = Root()
+ root.xmlrpc = XmlRpc()
+ cherrypy.tree.mount(root, config={'/': {
+ 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
+ 'tools.xmlrpc.allow_none': 0,
+ }})
+
+
+from cherrypy.test import helper
+
+class XmlRpcTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+ def testXmlRpc(self):
+
+ scheme = "http"
+ try:
+ scheme = self.harness.scheme
+ except AttributeError:
+ pass
+
+ 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")
+
+ # http://www.cherrypy.org/ticket/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")
+
diff --git a/cherrypy/test/webtest.py b/cherrypy/test/webtest.py
new file mode 100755
index 0000000..969eab0
--- /dev/null
+++ b/cherrypy/test/webtest.py
@@ -0,0 +1,535 @@
+"""Extensions to unittest for web frameworks.
+
+Use the WebCase.getPage method to request a page from your HTTP server.
+
+Framework Integration
+=====================
+
+If you have control over your server process, you can handle errors
+in the server-side of the HTTP conversation a bit better. You must run
+both the client (your WebCase tests) and the server in the same process
+(but in separate threads, obviously).
+
+When an error occurs in the framework, call server_error. It will print
+the traceback to stdout, and keep any assertions you have from running
+(the assumption is that, if the server errors, the page output will not
+be of further significance to your tests).
+"""
+
+import os
+import pprint
+import re
+import socket
+import sys
+import time
+import traceback
+import types
+
+from unittest import *
+from unittest import _TextTestResult
+
+from cherrypy._cpcompat import basestring, HTTPConnection, HTTPSConnection, unicodestr
+
+
+
+def interface(host):
+ """Return an IP address for a client connection given the server host.
+
+ If the server is listening on '0.0.0.0' (INADDR_ANY)
+ or '::' (IN6ADDR_ANY), this will return the proper localhost."""
+ if host == '0.0.0.0':
+ # INADDR_ANY, which should respond on localhost.
+ return "127.0.0.1"
+ if host == '::':
+ # IN6ADDR_ANY, which should respond on localhost.
+ return "::1"
+ return host
+
+
+class TerseTestResult(_TextTestResult):
+
+ def printErrors(self):
+ # Overridden to avoid unnecessary empty line
+ if self.errors or self.failures:
+ if self.dots or self.showAll:
+ self.stream.writeln()
+ self.printErrorList('ERROR', self.errors)
+ self.printErrorList('FAIL', self.failures)
+
+
+class TerseTestRunner(TextTestRunner):
+ """A test runner class that displays results in textual form."""
+
+ def _makeResult(self):
+ return TerseTestResult(self.stream, self.descriptions, self.verbosity)
+
+ def run(self, test):
+ "Run the given test case or test suite."
+ # Overridden to remove unnecessary empty lines and separators
+ result = self._makeResult()
+ test(result)
+ result.printErrors()
+ if not result.wasSuccessful():
+ self.stream.write("FAILED (")
+ failed, errored = list(map(len, (result.failures, result.errors)))
+ if failed:
+ self.stream.write("failures=%d" % failed)
+ if errored:
+ if failed: self.stream.write(", ")
+ self.stream.write("errors=%d" % errored)
+ self.stream.writeln(")")
+ return result
+
+
+class ReloadingTestLoader(TestLoader):
+
+ def loadTestsFromName(self, name, module=None):
+ """Return a suite of all tests cases given a string specifier.
+
+ The name may resolve either to a module, a test case class, a
+ test method within a test case class, or a callable object which
+ returns a TestCase or TestSuite instance.
+
+ The method optionally resolves the names relative to a given module.
+ """
+ parts = name.split('.')
+ unused_parts = []
+ if module is None:
+ if not parts:
+ raise ValueError("incomplete test name: %s" % name)
+ else:
+ parts_copy = parts[:]
+ while parts_copy:
+ target = ".".join(parts_copy)
+ if target in sys.modules:
+ module = reload(sys.modules[target])
+ parts = unused_parts
+ break
+ else:
+ try:
+ module = __import__(target)
+ parts = unused_parts
+ break
+ except ImportError:
+ unused_parts.insert(0,parts_copy[-1])
+ del parts_copy[-1]
+ if not parts_copy:
+ raise
+ parts = parts[1:]
+ obj = module
+ for part in parts:
+ obj = getattr(obj, part)
+
+ if type(obj) == types.ModuleType:
+ return self.loadTestsFromModule(obj)
+ elif (isinstance(obj, (type, types.ClassType)) and
+ issubclass(obj, TestCase)):
+ return self.loadTestsFromTestCase(obj)
+ elif type(obj) == types.UnboundMethodType:
+ return obj.im_class(obj.__name__)
+ elif hasattr(obj, '__call__'):
+ test = obj()
+ if not isinstance(test, TestCase) and \
+ not isinstance(test, TestSuite):
+ raise ValueError("calling %s returned %s, "
+ "not a test" % (obj,test))
+ return test
+ else:
+ raise ValueError("do not know how to make test from: %s" % obj)
+
+
+try:
+ # Jython support
+ if sys.platform[:4] == 'java':
+ def getchar():
+ # Hopefully this is enough
+ return sys.stdin.read(1)
+ else:
+ # On Windows, msvcrt.getch reads a single char without output.
+ import msvcrt
+ def getchar():
+ return msvcrt.getch()
+except ImportError:
+ # Unix getchr
+ import tty, 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 WebCase(TestCase):
+ HOST = "127.0.0.1"
+ PORT = 8000
+ HTTP_CONN = HTTPConnection
+ PROTOCOL = "HTTP/1.1"
+
+ scheme = "http"
+ url = None
+
+ status = None
+ headers = None
+ body = None
+
+ encoding = 'utf-8'
+
+ time = None
+
+ def get_conn(self, auto_open=False):
+ """Return a connection to our HTTP server."""
+ if self.scheme == "https":
+ cls = HTTPSConnection
+ else:
+ cls = HTTPConnection
+ conn = cls(self.interface(), self.PORT)
+ # Automatically re-connect?
+ conn.auto_open = auto_open
+ conn.connect()
+ return conn
+
+ def set_persistent(self, on=True, auto_open=False):
+ """Make our HTTP_CONN persistent (or not).
+
+ If the 'on' argument is True (the default), then self.HTTP_CONN
+ will be set to an instance of HTTPConnection (or HTTPS
+ if self.scheme is "https"). This will then persist across requests.
+
+ We only allow for a single open connection, so if you call this
+ and we currently have an open connection, it will be closed.
+ """
+ try:
+ self.HTTP_CONN.close()
+ except (TypeError, AttributeError):
+ pass
+
+ if on:
+ self.HTTP_CONN = self.get_conn(auto_open=auto_open)
+ else:
+ if self.scheme == "https":
+ self.HTTP_CONN = HTTPSConnection
+ else:
+ self.HTTP_CONN = HTTPConnection
+
+ def _get_persistent(self):
+ return hasattr(self.HTTP_CONN, "__class__")
+ def _set_persistent(self, on):
+ self.set_persistent(on)
+ persistent = property(_get_persistent, _set_persistent)
+
+ def interface(self):
+ """Return an IP address for a client connection.
+
+ If the server is listening on '0.0.0.0' (INADDR_ANY)
+ or '::' (IN6ADDR_ANY), this will return the proper localhost."""
+ return interface(self.HOST)
+
+ def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
+ """Open the url with debugging support. Return status, headers, body."""
+ ServerError.on = False
+
+ if isinstance(url, unicodestr):
+ url = url.encode('utf-8')
+ if isinstance(body, unicodestr):
+ body = body.encode('utf-8')
+
+ self.url = url
+ self.time = None
+ start = time.time()
+ result = openURL(url, headers, method, body, self.HOST, self.PORT,
+ self.HTTP_CONN, protocol or self.PROTOCOL)
+ self.time = time.time() - start
+ self.status, self.headers, self.body = result
+
+ # Build a list of request cookies from the previous response cookies.
+ self.cookies = [('Cookie', v) for k, v in self.headers
+ if k.lower() == 'set-cookie']
+
+ if ServerError.on:
+ raise ServerError()
+ return result
+
+ interactive = True
+ console_height = 30
+
+ def _handlewebError(self, msg):
+ print("")
+ print(" ERROR: %s" % msg)
+
+ if not self.interactive:
+ raise self.failureException(msg)
+
+ p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> "
+ sys.stdout.write(p)
+ sys.stdout.flush()
+ while True:
+ i = getchar().upper()
+ if i not in "BHSUIRX":
+ continue
+ print(i.upper()) # Also prints new line
+ if i == "B":
+ for x, line in enumerate(self.body.splitlines()):
+ 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)
+ elif i == "H":
+ pprint.pprint(self.headers)
+ elif i == "S":
+ print(self.status)
+ elif i == "U":
+ print(self.url)
+ 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)
+ sys.stdout.flush()
+
+ def exit(self):
+ sys.exit()
+
+ def assertStatus(self, status, msg=None):
+ """Fail if self.status != status."""
+ if isinstance(status, basestring):
+ if not self.status == status:
+ if msg is None:
+ msg = 'Status (%r) != %r' % (self.status, status)
+ self._handlewebError(msg)
+ elif isinstance(status, int):
+ code = int(self.status[:3])
+ if code != status:
+ if msg is None:
+ msg = 'Status (%r) != %r' % (self.status, status)
+ self._handlewebError(msg)
+ else:
+ # status is a tuple or list.
+ match = False
+ for s in status:
+ if isinstance(s, basestring):
+ if self.status == s:
+ match = True
+ break
+ elif int(self.status[:3]) == s:
+ match = True
+ break
+ if not match:
+ if msg is None:
+ msg = 'Status (%r) not in %r' % (self.status, status)
+ self._handlewebError(msg)
+
+ def assertHeader(self, key, value=None, msg=None):
+ """Fail if (key, [value]) not in self.headers."""
+ lowkey = key.lower()
+ for k, v in self.headers:
+ if k.lower() == lowkey:
+ if value is None or str(value) == v:
+ return v
+
+ if msg is None:
+ if value is None:
+ msg = '%r not in headers' % key
+ else:
+ msg = '%r:%r not in headers' % (key, value)
+ self._handlewebError(msg)
+
+ def assertHeaderItemValue(self, key, value, msg=None):
+ """Fail if the header does not contain the specified value"""
+ actual_value = self.assertHeader(key, msg=msg)
+ header_values = map(str.strip, actual_value.split(','))
+ if value in header_values:
+ return value
+
+ if msg is None:
+ msg = "%r not in %r" % (value, header_values)
+ self._handlewebError(msg)
+
+ def assertNoHeader(self, key, msg=None):
+ """Fail if key in self.headers."""
+ lowkey = key.lower()
+ matches = [k for k, v in self.headers if k.lower() == lowkey]
+ if matches:
+ if msg is None:
+ msg = '%r in headers' % key
+ self._handlewebError(msg)
+
+ def assertBody(self, value, msg=None):
+ """Fail if value != self.body."""
+ if value != self.body:
+ if msg is None:
+ msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body)
+ self._handlewebError(msg)
+
+ def assertInBody(self, value, msg=None):
+ """Fail if value not in self.body."""
+ if value not in self.body:
+ if msg is None:
+ msg = '%r not in body: %s' % (value, self.body)
+ self._handlewebError(msg)
+
+ def assertNotInBody(self, value, msg=None):
+ """Fail if value in self.body."""
+ if value in self.body:
+ if msg is None:
+ msg = '%r found in body' % value
+ self._handlewebError(msg)
+
+ def assertMatchesBody(self, pattern, msg=None, flags=0):
+ """Fail if value (a regex pattern) is not in self.body."""
+ if re.search(pattern, self.body, flags) is None:
+ if msg is None:
+ msg = 'No match for %r in body' % pattern
+ self._handlewebError(msg)
+
+
+methods_with_bodies = ("POST", "PUT")
+
+def cleanHeaders(headers, method, body, host, port):
+ """Return request headers, with required headers added (if missing)."""
+ if headers is None:
+ headers = []
+
+ # Add the required Host request header if not present.
+ # [This specifies the host:port of the server, not the client.]
+ found = False
+ for k, v in headers:
+ if k.lower() == 'host':
+ found = True
+ break
+ if not found:
+ if port == 80:
+ headers.append(("Host", host))
+ else:
+ headers.append(("Host", "%s:%s" % (host, port)))
+
+ if method in methods_with_bodies:
+ # Stick in default type and length headers if not present
+ found = False
+ for k, v in headers:
+ if k.lower() == 'content-type':
+ found = True
+ break
+ if not found:
+ headers.append(("Content-Type", "application/x-www-form-urlencoded"))
+ headers.append(("Content-Length", str(len(body or ""))))
+
+ return headers
+
+
+def shb(response):
+ """Return status, headers, body the way we like from a response."""
+ h = []
+ key, value = None, None
+ for line in response.msg.headers:
+ if line:
+ if line[0] in " \t":
+ value += line.strip()
+ else:
+ if key and value:
+ h.append((key, value))
+ key, value = line.split(":", 1)
+ key = key.strip()
+ value = value.strip()
+ if key and value:
+ h.append((key, value))
+
+ return "%s %s" % (response.status, response.reason), h, response.read()
+
+
+def openURL(url, headers=None, method="GET", body=None,
+ host="127.0.0.1", port=8000, http_conn=HTTPConnection,
+ protocol="HTTP/1.1"):
+ """Open the given HTTP resource and return status, headers, and body."""
+
+ headers = cleanHeaders(headers, method, body, host, port)
+
+ # Trying 10 times is simply in case of socket errors.
+ # Normal case--it should run once.
+ for trial in range(10):
+ try:
+ # Allow http_conn to be a class or an instance
+ if hasattr(http_conn, "host"):
+ conn = http_conn
+ else:
+ conn = http_conn(interface(host), port)
+
+ conn._http_vsn_str = protocol
+ conn._http_vsn = int("".join([x for x in protocol if x.isdigit()]))
+
+ # skip_accept_encoding argument added in python version 2.4
+ if sys.version_info < (2, 4):
+ def putheader(self, header, value):
+ if header == 'Accept-Encoding' and value == 'identity':
+ return
+ self.__class__.putheader(self, header, value)
+ import new
+ conn.putheader = new.instancemethod(putheader, conn, conn.__class__)
+ conn.putrequest(method.upper(), url, skip_host=True)
+ else:
+ conn.putrequest(method.upper(), url, skip_host=True,
+ skip_accept_encoding=True)
+
+ for key, value in headers:
+ conn.putheader(key, value)
+ conn.endheaders()
+
+ if body is not None:
+ conn.send(body)
+
+ # Handle response
+ response = conn.getresponse()
+
+ s, h, b = shb(response)
+
+ if not hasattr(http_conn, "host"):
+ # We made our own conn instance. Close it.
+ conn.close()
+
+ return s, h, b
+ except socket.error:
+ time.sleep(0.5)
+ raise
+
+
+# Add any exceptions which your web framework handles
+# normally (that you don't want server_error to trap).
+ignored_exceptions = []
+
+# You'll want set this to True when you can't guarantee
+# that each response will immediately follow each request;
+# for example, when handling requests via multiple threads.
+ignore_all = False
+
+class ServerError(Exception):
+ on = False
+
+
+def server_error(exc=None):
+ """Server debug hook. Return True if exception handled, False if ignored.
+
+ You probably want to wrap this, so you can still handle an error using
+ your framework when it's ignored.
+ """
+ if exc is None:
+ exc = sys.exc_info()
+
+ if ignore_all or exc[0] in ignored_exceptions:
+ return False
+ else:
+ ServerError.on = True
+ print("")
+ print("".join(traceback.format_exception(*exc)))
+ return True
+