Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/test/test_core.py

Revision 1992 (checked in by fumanchu, 4 weeks ago)

Test for #829 (@tools.response.headers doesn't appear to work with response.stream True).

  • Property svn:eol-style set to native
Line 
1 """Basic tests for the CherryPy core: request handling."""
2
3 from cherrypy.test import test
4 test.prefer_parent_path()
5
6 import os
7 localDir = os.path.dirname(__file__)
8 import sys
9 import types
10
11 import cherrypy
12 from cherrypy import _cptools, tools
13 from cherrypy.lib import http, static
14
15
16 favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico")
17
18 defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE",
19                         "TRACE", "CONNECT", "PROPFIND")
20
21
22 def setup_server():
23     class Root:
24        
25         def index(self):
26             return "hello"
27         index.exposed = True
28        
29         favicon_ico = tools.staticfile.handler(filename=favicon_path)
30        
31         def andnow(self):
32             return "the larch"
33         andnow.exposed = True
34        
35         def global_(self):
36             pass
37         global_.exposed = True
38        
39         def delglobal(self):
40             del self.__class__.__dict__['global_']
41         delglobal.exposed = True
42        
43         def defct(self, newct):
44             newct = "text/%s" % newct
45             cherrypy.config.update({'tools.response_headers.on': True,
46                                     'tools.response_headers.headers':
47                                     [('Content-Type', newct)]})
48         defct.exposed = True
49        
50         def upload(self, file):
51             return "Size: %s" % len(file.file.read())
52         upload.exposed = True
53        
54         def baseurl(self, path_info, relative=None):
55             return cherrypy.url(path_info, relative=bool(relative))
56         baseurl.exposed = True
57    
58     root = Root()
59    
60    
61     class TestType(type):
62         """Metaclass which automatically exposes all functions in each subclass,
63         and adds an instance of the subclass as an attribute of root.
64         """
65         def __init__(cls, name, bases, dct):
66             type.__init__(cls, name, bases, dct)
67             for value in dct.itervalues():
68                 if isinstance(value, types.FunctionType):
69                     value.exposed = True
70             setattr(root, name.lower(), cls())
71     class Test(object):
72         __metaclass__ = TestType
73    
74    
75     class URL(Test):
76        
77         _cp_config = {'tools.trailing_slash.on': False}
78        
79         def index(self, path_info, relative=None):
80             if relative != 'server':
81                 relative = bool(relative)
82             return cherrypy.url(path_info, relative=relative)
83        
84         def leaf(self, path_info, relative=None):
85             if relative != 'server':
86                 relative = bool(relative)
87             return cherrypy.url(path_info, relative=relative)
88    
89    
90     class Params(Test):
91        
92         def index(self, thing):
93             return repr(thing)
94        
95         def ismap(self, x, y):
96             return "Coordinates: %s, %s" % (x, y)
97        
98         def default(self, *args, **kwargs):
99             return "args: %s kwargs: %s" % (args, kwargs)
100
101
102     class Status(Test):
103        
104         def index(self):
105             return "normal"
106        
107         def blank(self):
108             cherrypy.response.status = ""
109        
110         # According to RFC 2616, new status codes are OK as long as they
111         # are between 100 and 599.
112        
113         # Here is an illegal code...
114         def illegal(self):
115             cherrypy.response.status = 781
116             return "oops"
117        
118         # ...and here is an unknown but legal code.
119         def unknown(self):
120             cherrypy.response.status = "431 My custom error"
121             return "funky"
122        
123         # Non-numeric code
124         def bad(self):
125             cherrypy.response.status = "error"
126             return "bad news"
127
128
129     class Redirect(Test):
130        
131         class Error:
132             _cp_config = {"tools.err_redirect.on": True,
133                           "tools.err_redirect.url": "/errpage",
134                           "tools.err_redirect.internal": False,
135                           }
136            
137             def index(self):
138                 raise NameError("redirect_test")
139             index.exposed = True
140         error = Error()
141        
142         def index(self):
143             return "child"
144        
145         def by_code(self, code):
146             raise cherrypy.HTTPRedirect("somewhere else", code)
147         by_code._cp_config = {'tools.trailing_slash.extra': True}
148        
149         def nomodify(self):
150             raise cherrypy.HTTPRedirect("", 304)
151        
152         def proxy(self):
153             raise cherrypy.HTTPRedirect("proxy", 305)
154        
155         def stringify(self):
156             return str(cherrypy.HTTPRedirect("/"))
157        
158         def fragment(self, frag):
159             raise cherrypy.HTTPRedirect("/some/url#%s" % frag)
160    
161     def login_redir():
162         if not getattr(cherrypy.request, "login", None):
163             raise cherrypy.InternalRedirect("/internalredirect/login")
164     tools.login_redir = _cptools.Tool('before_handler', login_redir)
165    
166     def redir_custom():
167         raise cherrypy.InternalRedirect("/internalredirect/custom_err")
168    
169     class InternalRedirect(Test):
170        
171         def index(self):
172             raise cherrypy.InternalRedirect("/")
173        
174         def choke(self):
175             return 3 / 0
176         choke.exposed = True
177         choke._cp_config = {'hooks.before_error_response': redir_custom}
178        
179         def relative(self, a, b):
180             raise cherrypy.InternalRedirect("cousin?t=6")
181        
182         def cousin(self, t):
183             assert cherrypy.request.prev.closed
184             return cherrypy.request.prev.query_string
185        
186         def petshop(self, user_id):
187             if user_id == "parrot":
188                 # Trade it for a slug when redirecting
189                 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug')
190             elif user_id == "terrier":
191                 # Trade it for a fish when redirecting
192                 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish')
193             else:
194                 # This should pass the user_id through to getImagesByUser
195                 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=%s' % user_id)
196        
197         # We support Python 2.3, but the @-deco syntax would look like this:
198         # @tools.login_redir()
199         def secure(self):
200             return "Welcome!"
201         secure = tools.login_redir()(secure)
202         # Since calling the tool returns the same function you pass in,
203         # you could skip binding the return value, and just write:
204         # tools.login_redir()(secure)
205        
206         def login(self):
207             return "Please log in"
208        
209         def custom_err(self):
210             return "Something went horribly wrong."
211        
212         def early_ir(self, arg):
213             return "whatever"
214         early_ir._cp_config = {'hooks.before_request_body': redir_custom}
215    
216    
217     class Image(Test):
218        
219         def getImagesByUser(self, user_id):
220             return "0 images for %s" % user_id
221
222
223     class Flatten(Test):
224        
225         def as_string(self):
226             return "content"
227        
228         def as_list(self):
229             return ["con", "tent"]
230        
231         def as_yield(self):
232             yield "content"
233        
234         def as_dblyield(self):
235             yield self.as_yield()
236         as_dblyield._cp_config = {'tools.flatten.on': True}
237        
238         def as_refyield(self):
239             for chunk in self.as_yield():
240                 yield chunk
241    
242    
243     def callable_error_page(status, **kwargs):
244         return "Error %s - Well, I'm very sorry but you haven't paid!" % status
245    
246    
247     class Error(Test):
248        
249         _cp_config = {'tools.log_tracebacks.on': True,
250                       }
251        
252         def custom(self, err='404'):
253             raise cherrypy.HTTPError(int(err), "No, <b>really</b>, not found!")
254         custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"),
255                              'error_page.401': callable_error_page,
256                              }
257        
258         def custom_default(self):
259             return 1 + 'a' # raise an unexpected error
260         custom_default._cp_config = {'error_page.default': callable_error_page}
261        
262         def noexist(self):
263             raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
264         noexist._cp_config = {'error_page.404': "nonexistent.html"}
265        
266         def page_method(self):
267             raise ValueError()
268        
269         def page_yield(self):
270             yield "howdy"
271             raise ValueError()
272        
273         def page_streamed(self):
274             yield "word up"
275             raise ValueError()
276             yield "very oops"
277         page_streamed._cp_config = {"response.stream": True}
278        
279         def cause_err_in_finalize(self):
280             # Since status must start with an int, this should error.
281             cherrypy.response.status = "ZOO OK"
282         cause_err_in_finalize._cp_config = {'request.show_tracebacks': False}
283        
284         def rethrow(self):
285             """Test that an error raised here will be thrown out to the server."""
286             raise ValueError()
287         rethrow._cp_config = {'request.throw_errors': True}
288    
289    
290     class Ranges(Test):
291        
292         def get_ranges(self, bytes):
293             return repr(http.get_ranges('bytes=%s' % bytes, 8))
294        
295         def slice_file(self):
296             path = os.path.join(os.getcwd(), os.path.dirname(__file__))
297             return static.serve_file(os.path.join(path, "static/index.html"))
298
299
300     class Expect(Test):
301        
302         def expectation_failed(self):
303             expect = cherrypy.request.headers.elements("Expect")
304             if expect and expect[0].value != '100-continue':
305                 raise cherrypy.HTTPError(400)
306             raise cherrypy.HTTPError(417, 'Expectation Failed')
307
308     class Headers(Test):
309        
310         def default(self, headername):
311             """Spit back out the value for the requested header."""
312             return cherrypy.request.headers[headername]
313        
314         def doubledheaders(self):
315             # From http://www.cherrypy.org/ticket/165:
316             # "header field names should not be case sensitive sayes the rfc.
317             # if i set a headerfield in complete lowercase i end up with two
318             # header fields, one in lowercase, the other in mixed-case."
319            
320             # Set the most common headers
321             hMap = cherrypy.response.headers
322             hMap['content-type'] = "text/html"
323             hMap['content-length'] = 18
324             hMap['server'] = 'CherryPy headertest'
325             hMap['location'] = ('%s://%s:%s/headers/'
326                                 % (cherrypy.request.local.ip,
327                                    cherrypy.request.local.port,
328                                    cherrypy.request.scheme))
329            
330             # Set a rare header for fun
331             hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
332            
333             return "double header test"
334        
335         def ifmatch(self):
336             val = cherrypy.request.headers['If-Match']
337             cherrypy.response.headers['ETag'] = val
338             return repr(val)
339    
340    
341     class HeaderElements(Test):
342        
343         def get_elements(self, headername):
344             e = cherrypy.request.headers.elements(headername)
345             return "\n".join([unicode(x) for x in e])
346    
347    
348     class Method(Test):
349        
350         def index(self):
351             m = cherrypy.request.method
352             if m in defined_http_methods:
353                 return m
354            
355             if m == "LINK":
356                 raise cherrypy.HTTPError(405)
357             else:
358                 raise cherrypy.HTTPError(501)
359        
360         def parameterized(self, data):
361             return data
362        
363         def request_body(self):
364             # This should be a file object (temp file),
365             # which CP will just pipe back out if we tell it to.
366             return cherrypy.request.body
367        
368         def reachable(self):
369             return "success"
370
371     class Divorce:
372         """HTTP Method handlers shouldn't collide with normal method names.
373         For example, a GET-handler shouldn't collide with a method named 'get'.
374         
375         If you build HTTP method dispatching into CherryPy, rewrite this class
376         to use your new dispatch mechanism and make sure that:
377             "GET /divorce HTTP/1.1" maps to divorce.index() and
378             "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
379         """
380        
381         documents = {}
382        
383         def index(self):
384             yield "<h1>Choose your document</h1>\n"
385             yield "<ul>\n"
386             for id, contents in self.documents.iteritems():
387                 yield ("    <li><a href='/divorce/get?ID=%s'>%s</a>: %s</li>\n"
388                        % (id, id, contents))
389             yield "</ul>"
390         index.exposed = True
391        
392         def get(self, ID):
393             return ("Divorce document %s: %s" %
394                     (ID, self.documents.get(ID, "empty")))
395         get.exposed = True
396
397     root.divorce = Divorce()
398
399
400     class Cookies(Test):
401        
402         def single(self, name):
403             cookie = cherrypy.request.cookie[name]
404             cherrypy.response.cookie[name] = cookie.value
405        
406         def multiple(self, names):
407             for name in names:
408                 cookie = cherrypy.request.cookie[name]
409                 cherrypy.response.cookie[name] = cookie.value
410
411
412     class ThreadLocal(Test):
413        
414         def index(self):
415             existing = repr(getattr(cherrypy.request, "asdf", None))
416             cherrypy.request.asdf = "rassfrassin"
417             return existing
418    
419     if sys.version_info >= (2, 5):
420         from cherrypy.test import py25
421         Root.expose_dec = py25.ExposeExamples()
422    
423     cherrypy.config.update({
424         'environment': 'test_suite',
425         'server.max_request_body_size': 200,
426         'server.max_request_header_size': 500,
427         })
428     appconf = {
429         '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")},
430         }
431     cherrypy.tree.mount(root, config=appconf)
432
433
434 #                             Client-side code                             #
435
436 from cherrypy.test import helper
437
438 class CoreRequestHandlingTest(helper.CPWebCase):
439    
440     def testParams(self):
441         self.getPage("/params/?thing=a")
442         self.assertBody("'a'")
443        
444         self.getPage("/params/?thing=a&thing=b&thing=c")
445         self.assertBody("['a', 'b', 'c']")
446        
447         # Test friendly error message when given params are not accepted.
448         ignore = helper.webtest.ignored_exceptions
449         ignore.append(TypeError)
450         try:
451             self.getPage("/params/?notathing=meeting")
452             self.assertInBody("index() got an unexpected keyword argument 'notathing'")
453         finally:
454             ignore.pop()
455        
456         # Test "% HEX HEX"-encoded URL, param keys, and values
457         self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
458         self.assertBody(r"args: ('\xd4 \xe3', 'cheese') "
459                         r"kwargs: {'Gruy\xe8re': 'Bulgn\xe9ville'}")
460        
461         # Make sure that encoded = and & get parsed correctly
462         self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2")
463         self.assertBody(r"args: ('code',) "
464                         r"kwargs: {'url': 'http://cherrypy.org/index?a=1&b=2'}")
465        
466         # Test coordinates sent by <img ismap>
467         self.getPage("/params/ismap?223,114")
468         self.assertBody("Coordinates: 223, 114")
469    
470     def testStatus(self):
471         self.getPage("/status/")
472         self.assertBody('normal')
473         self.assertStatus(200)
474        
475         self.getPage("/status/blank")
476         self.assertBody('')
477         self.assertStatus(200)
478        
479         self.getPage("/status/illegal")
480         self.assertStatus(500)
481         msg = "Illegal response status from server (781 is out of range)."
482         self.assertErrorPage(500, msg)
483        
484         self.getPage("/status/unknown")
485         self.assertBody('funky')
486         self.assertStatus(431)
487        
488         self.getPage("/status/bad")
489         self.assertStatus(500)
490         msg = "Illegal response status from server ('error' is non-numeric)."
491         self.assertErrorPage(500, msg)
492    
493     def testSlashes(self):
494         # Test that requests for index methods without a trailing slash
495         # get redirected to the same URI path with a trailing slash.
496         # Make sure GET params are preserved.
497         self.getPage("/redirect?id=3")
498         self.assertStatus(('302 Found', '303 See Other'))
499         self.assertInBody("<a href='%s/redirect/?id=3'>"
500                           "%s/redirect/?id=3</a>" % (self.base(), self.base()))
501        
502         if self.prefix():
503             # Corner case: the "trailing slash" redirect could be tricky if
504             # we're using a virtual root and the URI is "/vroot" (no slash).
505             self.getPage("")
506             self.assertStatus(('302 Found', '303 See Other'))
507             self.assertInBody("<a href='%s/'>%s/</a>" %
508                               (self.base(), self.base()))
509        
510         # Test that requests for NON-index methods WITH a trailing slash
511         # get redirected to the same URI path WITHOUT a trailing slash.
512         # Make sure GET params are preserved.
513         self.getPage("/redirect/by_code/?code=307"