mirror of https://github.com/evilhero/mylar
618 lines
24 KiB
Python
Executable File
618 lines
24 KiB
Python
Executable File
"""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")
|
|
|