1 """Basic tests for the CherryPy core: request handling."""
4 localDir
= os
.path
.dirname(__file__
)
9 from cherrypy
._cpcompat
import IncompleteRead
, itervalues
, ntob
10 from cherrypy
import _cptools
, tools
11 from cherrypy
.lib
import httputil
, static
14 favicon_path
= os
.path
.join(os
.getcwd(), localDir
, "../favicon.ico")
18 from cherrypy
.test
import helper
20 class CoreRequestHandlingTest(helper
.CPWebCase
):
29 favicon_ico
= tools
.staticfile
.handler(filename
=favicon_path
)
31 def defct(self
, newct
):
32 newct
= "text/%s" % newct
33 cherrypy
.config
.update({'tools.response_headers.on': True,
34 'tools.response_headers.headers':
35 [('Content-Type', newct
)]})
38 def baseurl(self
, path_info
, relative
=None):
39 return cherrypy
.url(path_info
, relative
=bool(relative
))
40 baseurl
.exposed
= True
44 if sys
.version_info
>= (2, 5):
45 from cherrypy
.test
._test
_decorators
import ExposeExamples
46 root
.expose_dec
= ExposeExamples()
50 """Metaclass which automatically exposes all functions in each subclass,
51 and adds an instance of the subclass as an attribute of root.
53 def __init__(cls
, name
, bases
, dct
):
54 type.__init
__(cls
, name
, bases
, dct
)
55 for value
in itervalues(dct
):
56 if isinstance(value
, types
.FunctionType
):
58 setattr(root
, name
.lower(), cls())
60 __metaclass__
= TestType
65 _cp_config
= {'tools.trailing_slash.on': False}
67 def index(self
, path_info
, relative
=None):
68 if relative
!= 'server':
69 relative
= bool(relative
)
70 return cherrypy
.url(path_info
, relative
=relative
)
72 def leaf(self
, path_info
, relative
=None):
73 if relative
!= 'server':
74 relative
= bool(relative
)
75 return cherrypy
.url(path_info
, relative
=relative
)
84 cherrypy
.response
.status
= ""
86 # According to RFC 2616, new status codes are OK as long as they
87 # are between 100 and 599.
89 # Here is an illegal code...
91 cherrypy
.response
.status
= 781
94 # ...and here is an unknown but legal code.
96 cherrypy
.response
.status
= "431 My custom error"
101 cherrypy
.response
.status
= "error"
105 class Redirect(Test
):
108 _cp_config
= {"tools.err_redirect.on": True,
109 "tools.err_redirect.url": "/errpage",
110 "tools.err_redirect.internal": False,
114 raise NameError("redirect_test")
121 def custom(self
, url
, code
):
122 raise cherrypy
.HTTPRedirect(url
, code
)
124 def by_code(self
, code
):
125 raise cherrypy
.HTTPRedirect("somewhere%20else", code
)
126 by_code
._cp
_config
= {'tools.trailing_slash.extra': True}
129 raise cherrypy
.HTTPRedirect("", 304)
132 raise cherrypy
.HTTPRedirect("proxy", 305)
135 return str(cherrypy
.HTTPRedirect("/"))
137 def fragment(self
, frag
):
138 raise cherrypy
.HTTPRedirect("/some/url#%s" % frag
)
141 if not getattr(cherrypy
.request
, "login", None):
142 raise cherrypy
.InternalRedirect("/internalredirect/login")
143 tools
.login_redir
= _cptools
.Tool('before_handler', login_redir
)
146 raise cherrypy
.InternalRedirect("/internalredirect/custom_err")
148 class InternalRedirect(Test
):
151 raise cherrypy
.InternalRedirect("/")
156 choke
._cp
_config
= {'hooks.before_error_response': redir_custom
}
158 def relative(self
, a
, b
):
159 raise cherrypy
.InternalRedirect("cousin?t=6")
162 assert cherrypy
.request
.prev
.closed
163 return cherrypy
.request
.prev
.query_string
165 def petshop(self
, user_id
):
166 if user_id
== "parrot":
167 # Trade it for a slug when redirecting
168 raise cherrypy
.InternalRedirect('/image/getImagesByUser?user_id=slug')
169 elif user_id
== "terrier":
170 # Trade it for a fish when redirecting
171 raise cherrypy
.InternalRedirect('/image/getImagesByUser?user_id=fish')
173 # This should pass the user_id through to getImagesByUser
174 raise cherrypy
.InternalRedirect(
175 '/image/getImagesByUser?user_id=%s' % str(user_id
))
177 # We support Python 2.3, but the @-deco syntax would look like this:
178 # @tools.login_redir()
181 secure
= tools
.login_redir()(secure
)
182 # Since calling the tool returns the same function you pass in,
183 # you could skip binding the return value, and just write:
184 # tools.login_redir()(secure)
187 return "Please log in"
189 def custom_err(self
):
190 return "Something went horribly wrong."
192 def early_ir(self
, arg
):
194 early_ir
._cp
_config
= {'hooks.before_request_body': redir_custom
}
199 def getImagesByUser(self
, user_id
):
200 return "0 images for %s" % user_id
209 return ["con", "tent"]
212 yield ntob("content")
214 def as_dblyield(self
):
215 yield self
.as_yield()
216 as_dblyield
._cp
_config
= {'tools.flatten.on': True}
218 def as_refyield(self
):
219 for chunk
in self
.as_yield():
225 def get_ranges(self
, bytes
):
226 return repr(httputil
.get_ranges('bytes=%s' % bytes
, 8))
228 def slice_file(self
):
229 path
= os
.path
.join(os
.getcwd(), os
.path
.dirname(__file__
))
230 return static
.serve_file(os
.path
.join(path
, "static/index.html"))
235 def single(self
, name
):
236 cookie
= cherrypy
.request
.cookie
[name
]
237 # Python2's SimpleCookie.__setitem__ won't take unicode keys.
238 cherrypy
.response
.cookie
[str(name
)] = cookie
.value
240 def multiple(self
, names
):
242 cookie
= cherrypy
.request
.cookie
[name
]
243 # Python2's SimpleCookie.__setitem__ won't take unicode keys.
244 cherrypy
.response
.cookie
[str(name
)] = cookie
.value
247 cherrypy
.tree
.mount(root
)
248 setup_server
= staticmethod(setup_server
)
251 def testStatus(self
):
252 self
.getPage("/status/")
253 self
.assertBody('normal')
254 self
.assertStatus(200)
256 self
.getPage("/status/blank")
258 self
.assertStatus(200)
260 self
.getPage("/status/illegal")
261 self
.assertStatus(500)
262 msg
= "Illegal response status from server (781 is out of range)."
263 self
.assertErrorPage(500, msg
)
265 if not getattr(cherrypy
.server
, 'using_apache', False):
266 self
.getPage("/status/unknown")
267 self
.assertBody('funky')
268 self
.assertStatus(431)
270 self
.getPage("/status/bad")
271 self
.assertStatus(500)
272 msg
= "Illegal response status from server ('error' is non-numeric)."
273 self
.assertErrorPage(500, msg
)
275 def testSlashes(self
):
276 # Test that requests for index methods without a trailing slash
277 # get redirected to the same URI path with a trailing slash.
278 # Make sure GET params are preserved.
279 self
.getPage("/redirect?id=3")
280 self
.assertStatus(301)
281 self
.assertInBody("<a href='%s/redirect/?id=3'>"
282 "%s/redirect/?id=3</a>" % (self
.base(), self
.base()))
285 # Corner case: the "trailing slash" redirect could be tricky if
286 # we're using a virtual root and the URI is "/vroot" (no slash).
288 self
.assertStatus(301)
289 self
.assertInBody("<a href='%s/'>%s/</a>" %
290 (self
.base(), self
.base()))
292 # Test that requests for NON-index methods WITH a trailing slash
293 # get redirected to the same URI path WITHOUT a trailing slash.
294 # Make sure GET params are preserved.
295 self
.getPage("/redirect/by_code/?code=307")
296 self
.assertStatus(301)
297 self
.assertInBody("<a href='%s/redirect/by_code?code=307'>"
298 "%s/redirect/by_code?code=307</a>"
299 % (self
.base(), self
.base()))
301 # If the trailing_slash tool is off, CP should just continue
302 # as if the slashes were correct. But it needs some help
303 # inside cherrypy.url to form correct output.
304 self
.getPage('/url?path_info=page1')
305 self
.assertBody('%s/url/page1' % self
.base())
306 self
.getPage('/url/leaf/?path_info=page1')
307 self
.assertBody('%s/url/page1' % self
.base())
309 def testRedirect(self
):
310 self
.getPage("/redirect/")
311 self
.assertBody('child')
312 self
.assertStatus(200)
314 self
.getPage("/redirect/by_code?code=300")
315 self
.assertMatchesBody(r
"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
316 self
.assertStatus(300)
318 self
.getPage("/redirect/by_code?code=301")
319 self
.assertMatchesBody(r
"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
320 self
.assertStatus(301)
322 self
.getPage("/redirect/by_code?code=302")
323 self
.assertMatchesBody(r
"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
324 self
.assertStatus(302)
326 self
.getPage("/redirect/by_code?code=303")
327 self
.assertMatchesBody(r
"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
328 self
.assertStatus(303)
330 self
.getPage("/redirect/by_code?code=307")
331 self
.assertMatchesBody(r
"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
332 self
.assertStatus(307)
334 self
.getPage("/redirect/nomodify")
336 self
.assertStatus(304)
338 self
.getPage("/redirect/proxy")
340 self
.assertStatus(305)
342 # HTTPRedirect on error
343 self
.getPage("/redirect/error/")
344 self
.assertStatus(('302 Found', '303 See Other'))
345 self
.assertInBody('/errpage')
347 # Make sure str(HTTPRedirect()) works.
348 self
.getPage("/redirect/stringify", protocol
="HTTP/1.0")
349 self
.assertStatus(200)
350 self
.assertBody("(['%s/'], 302)" % self
.base())
351 if cherrypy
.server
.protocol_version
== "HTTP/1.1":
352 self
.getPage("/redirect/stringify", protocol
="HTTP/1.1")
353 self
.assertStatus(200)
354 self
.assertBody("(['%s/'], 303)" % self
.base())
356 # check that #fragments are handled properly
357 # http://skrb.org/ietf/http_errata.html#location-fragments
359 self
.getPage("/redirect/fragment/%s" % frag
)
360 self
.assertMatchesBody(r
"<a href='(.*)\/some\/url\#%s'>\1\/some\/url\#%s</a>" % (frag
, frag
))
361 loc
= self
.assertHeader('Location')
362 assert loc
.endswith("#%s" % frag
)
363 self
.assertStatus(('302 Found', '303 See Other'))
365 # check injection protection
366 # See http://www.cherrypy.org/ticket/1003
367 self
.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval")
368 self
.assertStatus(303)
369 loc
= self
.assertHeader('Location')
370 assert 'Set-Cookie' in loc
371 self
.assertNoHeader('Set-Cookie')
373 def test_InternalRedirect(self
):
375 self
.getPage("/internalredirect/")
376 self
.assertBody('hello')
377 self
.assertStatus(200)
380 self
.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
381 self
.assertBody('0 images for Sir-not-appearing-in-this-film')
382 self
.assertStatus(200)
385 self
.getPage("/internalredirect/petshop?user_id=parrot")
386 self
.assertBody('0 images for slug')
387 self
.assertStatus(200)
390 self
.getPage("/internalredirect/petshop", method
="POST",
391 body
="user_id=terrier")
392 self
.assertBody('0 images for fish')
393 self
.assertStatus(200)
395 # Test ir before body read
396 self
.getPage("/internalredirect/early_ir", method
="POST",
398 self
.assertBody("Something went horribly wrong.")
399 self
.assertStatus(200)
401 self
.getPage("/internalredirect/secure")
402 self
.assertBody('Please log in')
403 self
.assertStatus(200)
405 # Relative path in InternalRedirect.
406 # Also tests request.prev.
407 self
.getPage("/internalredirect/relative?a=3&b=5")
408 self
.assertBody("a=3&b=5")
409 self
.assertStatus(200)
411 # InternalRedirect on error
412 self
.getPage("/internalredirect/choke")
413 self
.assertStatus(200)
414 self
.assertBody("Something went horribly wrong.")
416 def testFlatten(self
):
417 for url
in ["/flatten/as_string", "/flatten/as_list",
418 "/flatten/as_yield", "/flatten/as_dblyield",
419 "/flatten/as_refyield"]:
421 self
.assertBody('content')
423 def testRanges(self
):
424 self
.getPage("/ranges/get_ranges?bytes=3-6")
425 self
.assertBody("[(3, 7)]")
427 # Test multiple ranges and a suffix-byte-range-spec, for good measure.
428 self
.getPage("/ranges/get_ranges?bytes=2-4,-1")
429 self
.assertBody("[(2, 5), (7, 8)]")
431 # Get a partial file.
432 if cherrypy
.server
.protocol_version
== "HTTP/1.1":
433 self
.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
434 self
.assertStatus(206)
435 self
.assertHeader("Content-Type", "text/html;charset=utf-8")
436 self
.assertHeader("Content-Range", "bytes 2-5/14")
437 self
.assertBody("llo,")
439 # What happens with overlapping ranges (and out of order, too)?
440 self
.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')])
441 self
.assertStatus(206)
442 ct
= self
.assertHeader("Content-Type")
443 expected_type
= "multipart/byteranges; boundary="
444 self
.assert_(ct
.startswith(expected_type
))
445 boundary
= ct
[len(expected_type
):]
446 expected_body
= ("\r\n--%s\r\n"
447 "Content-type: text/html\r\n"
448 "Content-range: bytes 4-6/14\r\n"
452 "Content-type: text/html\r\n"
453 "Content-range: bytes 2-5/14\r\n"
456 "--%s--\r\n" % (boundary
, boundary
, boundary
))
457 self
.assertBody(expected_body
)
458 self
.assertHeader("Content-Length")
460 # Test "416 Requested Range Not Satisfiable"
461 self
.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')])
462 self
.assertStatus(416)
463 # "When this status code is returned for a byte-range request,
464 # the response SHOULD include a Content-Range entity-header
465 # field specifying the current length of the selected resource"
466 self
.assertHeader("Content-Range", "bytes */14")
467 elif cherrypy
.server
.protocol_version
== "HTTP/1.0":
468 # Test Range behavior with HTTP/1.0 request
469 self
.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
470 self
.assertStatus(200)
471 self
.assertBody("Hello, world\r\n")
473 def testFavicon(self
):
474 # favicon.ico is served by staticfile.
475 icofilename
= os
.path
.join(localDir
, "../favicon.ico")
476 icofile
= open(icofilename
, "rb")
477 data
= icofile
.read()
480 self
.getPage("/favicon.ico")
481 self
.assertBody(data
)
483 def testCookies(self
):
484 if sys
.version_info
>= (2, 5):
485 header_value
= lambda x
: x
487 header_value
= lambda x
: x
+';'
489 self
.getPage("/cookies/single?name=First",
490 [('Cookie', 'First=Dinsdale;')])
491 self
.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
493 self
.getPage("/cookies/multiple?names=First&names=Last",
494 [('Cookie', 'First=Dinsdale; Last=Piranha;'),
496 self
.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
497 self
.assertHeader('Set-Cookie', header_value('Last=Piranha'))
499 self
.getPage("/cookies/single?name=Something-With:Colon",
500 [('Cookie', 'Something-With:Colon=some-value')])
501 self
.assertStatus(400)
503 def testDefaultContentType(self
):
505 self
.assertHeader('Content-Type', 'text/html;charset=utf-8')
506 self
.getPage('/defct/plain')
508 self
.assertHeader('Content-Type', 'text/plain;charset=utf-8')
509 self
.getPage('/defct/html')
511 def test_cherrypy_url(self
):
512 # Input relative to current
513 self
.getPage('/url/leaf?path_info=page1')
514 self
.assertBody('%s/url/page1' % self
.base())
515 self
.getPage('/url/?path_info=page1')
516 self
.assertBody('%s/url/page1' % self
.base())
518 host
= 'www.mydomain.example'
519 self
.getPage('/url/leaf?path_info=page1',
520 headers
=[('Host', host
)])
521 self
.assertBody('%s://%s/url/page1' % (self
.scheme
, host
))
523 # Input is 'absolute'; that is, relative to script_name
524 self
.getPage('/url/leaf?path_info=/page1')
525 self
.assertBody('%s/page1' % self
.base())
526 self
.getPage('/url/?path_info=/page1')
527 self
.assertBody('%s/page1' % self
.base())
530 self
.getPage('/url/leaf?path_info=./page1')
531 self
.assertBody('%s/url/page1' % self
.base())
532 self
.getPage('/url/leaf?path_info=other/./page1')
533 self
.assertBody('%s/url/other/page1' % self
.base())
534 self
.getPage('/url/?path_info=/other/./page1')
535 self
.assertBody('%s/other/page1' % self
.base())
538 self
.getPage('/url/leaf?path_info=../page1')
539 self
.assertBody('%s/page1' % self
.base())
540 self
.getPage('/url/leaf?path_info=other/../page1')
541 self
.assertBody('%s/url/page1' % self
.base())
542 self
.getPage('/url/leaf?path_info=/other/../page1')
543 self
.assertBody('%s/page1' % self
.base())
545 # Output relative to current path or script_name
546 self
.getPage('/url/?path_info=page1&relative=True')
547 self
.assertBody('page1')
548 self
.getPage('/url/leaf?path_info=/page1&relative=True')
549 self
.assertBody('../page1')
550 self
.getPage('/url/leaf?path_info=page1&relative=True')
551 self
.assertBody('page1')
552 self
.getPage('/url/leaf?path_info=leaf/page1&relative=True')
553 self
.assertBody('leaf/page1')
554 self
.getPage('/url/leaf?path_info=../page1&relative=True')
555 self
.assertBody('../page1')
556 self
.getPage('/url/?path_info=other/../page1&relative=True')
557 self
.assertBody('page1')
559 # Output relative to /
560 self
.getPage('/baseurl?path_info=ab&relative=True')
561 self
.assertBody('ab')
562 # Output relative to /
563 self
.getPage('/baseurl?path_info=/ab&relative=True')
564 self
.assertBody('ab')
566 # absolute-path references ("server-relative")
567 # Input relative to current
568 self
.getPage('/url/leaf?path_info=page1&relative=server')
569 self
.assertBody('/url/page1')
570 self
.getPage('/url/?path_info=page1&relative=server')
571 self
.assertBody('/url/page1')
572 # Input is 'absolute'; that is, relative to script_name
573 self
.getPage('/url/leaf?path_info=/page1&relative=server')
574 self
.assertBody('/page1')
575 self
.getPage('/url/?path_info=/page1&relative=server')
576 self
.assertBody('/page1')
578 def test_expose_decorator(self
):
579 if not sys
.version_info
>= (2, 5):
580 return self
.skip("skipped (Python 2.5+ only) ")
583 self
.getPage("/expose_dec/no_call")
584 self
.assertStatus(200)
585 self
.assertBody("Mr E. R. Bradshaw")
588 self
.getPage("/expose_dec/call_empty")
589 self
.assertStatus(200)
590 self
.assertBody("Mrs. B.J. Smegma")
592 # Test @expose("alias")
593 self
.getPage("/expose_dec/call_alias")
594 self
.assertStatus(200)
595 self
.assertBody("Mr Nesbitt")
596 # Does the original name work?
597 self
.getPage("/expose_dec/nesbitt")
598 self
.assertStatus(200)
599 self
.assertBody("Mr Nesbitt")
601 # Test @expose(["alias1", "alias2"])
602 self
.getPage("/expose_dec/alias1")
603 self
.assertStatus(200)
604 self
.assertBody("Mr Ken Andrews")
605 self
.getPage("/expose_dec/alias2")
606 self
.assertStatus(200)
607 self
.assertBody("Mr Ken Andrews")
608 # Does the original name work?
609 self
.getPage("/expose_dec/andrews")
610 self
.assertStatus(200)
611 self
.assertBody("Mr Ken Andrews")
613 # Test @expose(alias="alias")
614 self
.getPage("/expose_dec/alias3")
615 self
.assertStatus(200)
616 self
.assertBody("Mr. and Mrs. Watson")