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

root/branches/cherrypy-3.0.x/cherrypy/test/test_core.py

Revision 1981 (checked in by fumanchu, 6 months ago)

Backport of [1717]. See #727.

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