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

root/branches/cherrypy-2.1/cherrypy/test/test_core.py

Revision 1318 (checked in by fumanchu, 2 years ago)

Fix to 2.1, 2.2, 3.0 for bugs in Range slicing and final boundary. Also made the output match Apache output (CRLFs).

Line 
1 """
2 Copyright (c) 2005, CherryPy Team (team@cherrypy.org)
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8     * Redistributions of source code must retain the above copyright notice,
9       this list of conditions and the following disclaimer.
10     * Redistributions in binary form must reproduce the above copyright notice,
11       this list of conditions and the following disclaimer in the documentation
12       and/or other materials provided with the distribution.
13     * Neither the name of the CherryPy Team nor the names of its contributors
14       may be used to endorse or promote products derived from this software
15       without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 """
28
29 """Basic tests for the CherryPy core: request handling."""
30
31 import cherrypy
32 from cherrypy.lib import cptools
33 import types
34 import os
35 localDir = os.path.dirname(__file__)
36
37
38 class Root:
39    
40     def index(self):
41         return "hello"
42     index.exposed = True
43    
44     def _global(self):
45         pass
46     _global.exposed = True
47
48 cherrypy.root = Root()
49
50
51 class TestType(type):
52     def __init__(cls, name, bases, dct):
53         type.__init__(name, bases, dct)
54         for value in dct.itervalues():
55             if isinstance(value, types.FunctionType):
56                 value.exposed = True
57         setattr(cherrypy.root, name.lower(), cls())
58 class Test(object):
59     __metaclass__ = TestType
60
61
62 class Params(Test):
63    
64     def index(self, thing):
65         return repr(thing)
66    
67     def ismap(self, x, y):
68         return "Coordinates: %s, %s" % (x, y)
69    
70     def default(self, *args, **kwargs):
71         return "args: %s kwargs: %s" % (args, kwargs)
72
73
74 class Status(Test):
75    
76     def index(self):
77         return "normal"
78    
79     def blank(self):
80         cherrypy.response.status = ""
81    
82     # According to RFC 2616, new status codes are OK as long as they
83     # are between 100 and 599.
84    
85     # Here is an illegal code...
86     def illegal(self):
87         cherrypy.response.status = 781
88         return "oops"
89    
90     # ...and here is an unknown but legal code.
91     def unknown(self):
92         cherrypy.response.status = "431 My custom error"
93         return "funky"
94    
95     # Non-numeric code
96     def bad(self):
97         cherrypy.response.status = "error"
98         return "hello"
99
100
101 class Redirect(Test):
102    
103     def _cpOnError(self):
104         raise cherrypy.HTTPRedirect("/errpage")
105    
106     def error(self):
107         raise NameError()
108    
109     def index(self):
110         return "child"
111    
112     def by_code(self, code):
113         raise cherrypy.HTTPRedirect("somewhere else", code)
114    
115     def nomodify(self):
116         raise cherrypy.HTTPRedirect("", 304)
117    
118     def proxy(self):
119         raise cherrypy.HTTPRedirect("proxy", 305)
120    
121     def internal(self):
122         raise cherrypy.InternalRedirect("/")
123    
124     def internal2(self, user_id):
125         if user_id == "parrot":
126             # Trade it for a slug when redirecting
127             raise cherrypy.InternalRedirect('/image/getImagesByUser',
128                                            "user_id=slug")
129         elif user_id == "terrier":
130             # Trade it for a fish when redirecting
131             raise cherrypy.InternalRedirect('/image/getImagesByUser',
132                                            {"user_id": "fish"})
133         else:
134             raise cherrypy.InternalRedirect('/image/getImagesByUser')
135
136
137 class Image(Test):
138    
139     def getImagesByUser(self, user_id):
140         return "0 images for %s" % user_id
141
142
143 class Flatten(Test):
144    
145     def as_string(self):
146         return "content"
147    
148     def as_list(self):
149         return ["con", "tent"]
150    
151     def as_yield(self):
152         yield "content"
153    
154     def as_dblyield(self):
155         yield self.as_yield()
156    
157     def as_refyield(self):
158         for chunk in self.as_yield():
159             yield chunk
160
161
162 class Error(Test):
163    
164     def custom(self):
165         raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
166    
167     def page_method(self):
168         raise ValueError()
169    
170     def page_yield(self):
171         yield "hello"
172         raise ValueError()
173    
174     def page_streamed(self):
175         yield "hello"
176         raise ValueError()
177         yield "very oops"
178    
179     def cause_err_in_finalize(self):
180         # Since status must start with an int, this should error.
181         cherrypy.response.status = "ZOO OK"
182
183
184 class Ranges(Test):
185    
186     def get_ranges(self):
187         return repr(cptools.getRanges(8))
188    
189     def slice_file(self):
190         path = os.path.join(os.getcwd(), os.path.dirname(__file__))
191         return cptools.serveFile(os.path.join(path, "static/index.html"))
192
193
194 class Headers(Test):
195    
196     def index(self):
197         # From http://www.cherrypy.org/ticket/165:
198         # "header field names should not be case sensitive sayes the rfc.
199         # if i set a headerfield in complete lowercase i end up with two
200         # header fields, one in lowercase, the other in mixed-case."
201        
202         # Set the most common headers
203         hMap = cherrypy.response.headerMap
204         hMap['content-type'] = "text/html"
205         hMap['content-length'] = 18
206         hMap['server'] = 'CherryPy headertest'
207         hMap['location'] = ('%s://127.0.0.1:%s/headers/'
208                             % (cherrypy.request.remotePort,
209                                cherrypy.request.scheme))
210        
211         # Set a rare header for fun
212         hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
213        
214         return "double header test"
215
216
217 defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE",
218                         "TRACE", "CONNECT")
219 class Method(Test):
220    
221     def index(self):
222         m = cherrypy.request.method
223         if m in defined_http_methods:
224             return m
225        
226         if m == "LINK":
227             raise cherrypy.HTTPError(405)
228         else:
229             raise cherrypy.HTTPError(501)
230    
231     def parameterized(self, data):
232         return data
233    
234     def request_body(self):
235         # This should be a file object (temp file),
236         # which CP will just pipe back out if we tell it to.
237         return cherrypy.request.body
238
239 class Cookies(Test):
240    
241     def single(self, name):
242         cookie = cherrypy.request.simpleCookie[name]
243         cherrypy.response.simpleCookie[name] = cookie.value
244    
245     def multiple(self, names):
246         for name in names:
247             cookie = cherrypy.request.simpleCookie[name]
248             cherrypy.response.simpleCookie[name] = cookie.value
249
250 class MaxRequestSize(Test):
251    
252     def index(self):
253         return "OK"
254
255     def upload(self, file):
256         return "Size: %s" % len(file.file.read())
257
258 class ThreadLocal(Test):
259    
260     def index(self):
261         existing = repr(getattr(cherrypy.request, "asdf", None))
262         cherrypy.request.asdf = "hello"
263         return existing
264
265
266 logFile = os.path.join(localDir, "error.log")
267 logAccessFile = os.path.join(localDir, "access.log")
268
269 cherrypy.config.update({
270     'global': {'server.logToScreen': False,
271                'server.environment': 'production',
272                'server.showTracebacks': True,
273                'server.protocolVersion': "HTTP/1.1",
274                },
275     '/': {
276         'foo': 'this',
277         'bar': 'that',
278     },
279     '/foo': {
280         'foo': 'this2',
281         'baz': 'that2',
282     },
283     '/foo/bar': {
284         'foo': 'this3',
285         'bax': 'this4',
286     },
287     '/flatten': {
288         'server.logFile': logFile,
289         'server.logAccessFile': logAccessFile,
290     },
291     '/params': {
292         'server.logFile': logFile,
293     },
294     '/error': {
295         'server.logFile': logFile,
296         'server.logTracebacks': True,
297     },
298     '/error/page_streamed': {
299         'streamResponse': True,
300     },
301     '/error/cause_err_in_finalize': {
302         'server.showTracebacks': False,
303     },
304     '/error/custom': {
305         'errorPage.404': "nonexistent.html",
306     },
307 })
308
309 # Shortcut syntax--should get put in the "global" bucket
310 cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove'})
311
312 import helper
313
314 class CoreRequestHandlingTest(helper.CPWebCase):
315    
316     def testConfig(self):
317         tests = [
318             ('global',   'luxuryyacht', 'throatwobblermangrove'),
319             ('/',        'nex', None   ),
320             ('/',        'foo', 'this' ),
321             ('/',        'bar', 'that' ),
322             ('/xyz',     'foo', 'this' ),
323             ('/foo',     'foo', 'this2'),
324             ('/foo',     'bar', 'that' ),
325             ('/foo',     'bax', None   ),
326             ('/foo/bar', 'baz', 'that2'),
327             ('/foo/nex', 'baz', 'that2'),
328         ]
329         cherrypy.request.purge__()
330         for path, key, expected in tests:
331             cherrypy.request.path = path
332             result = cherrypy.config.get(key, None)
333             self.assertEqual(result, expected)
334    
335     def testParams(self):
336         self.getPage("/params/?thing=a")
337         self.assertBody("'a'")
338        
339         self.getPage("/params/?thing=a&thing=b&thing=c")
340         self.assertBody("['a', 'b', 'c']")
341        
342         # Test friendly error message when given params are not accepted.
343         ignore = helper.webtest.ignored_exceptions
344         ignore.append(TypeError)
345         try:
346             self.getPage("/params/?notathing=meeting")
347             self.assertInBody("TypeError: index() got an unexpected keyword argument 'notathing'")
348         finally:
349             ignore.pop()
350        
351         # Test "% HEX HEX"-encoded URL, param keys, and values
352         self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
353         self.assertBody(r"args: ('\xd4 \xe3', 'cheese') "
354                         r"kwargs: {'Gruy\xe8re': 'Bulgn\xe9ville'}")
355        
356         # Make sure that encoded = and & get parsed correctly
357         self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2")
358         self.assertBody(r"args: ('code',) "
359                         r"kwargs: {'url': 'http://cherrypy.org/index?a=1&b=2'}")
360        
361         # Test coordinates sent by <img ismap>
362         self.getPage("/params/ismap?223,114")
363         self.assertBody("Coordinates: 223, 114")
364    
365     def testStatus(self):
366         self.getPage("/status/")
367         self.assertBody('normal')
368         self.assertStatus('200 OK')
369        
370         self.getPage("/status/blank")
371         self.assertBody('')
372         self.assertStatus('200 OK')
373        
374         self.getPage("/status/illegal")
375         self.assertStatus('500 Internal error')
376         msg = "Illegal response status from server (out of range)."
377         self.assertErrorPage(500, msg)
378        
379         self.getPage("/status/unknown")
380         self.assertBody('funky')
381         self.assertStatus('431 My custom error')
382        
383         self.getPage("/status/bad")
384         self.assertStatus('500 Internal error')
385         msg = "Illegal response status from server (non-numeric)."
386         self.assertErrorPage(500, msg)
387    
388     def testLogging(self):
389         open(logFile, "wb").write("")
390         open(logAccessFile, "wb").write("")
391        
392         self.getPage("/flatten/as_string")
393         self.assertBody('content')
394         self.assertStatus('200 OK')
395        
396         self.getPage("/flatten/as_yield")
397         self.assertBody('content')
398         self.assertStatus('200 OK')
399        
400         data = open(logAccessFile, "rb").readlines()
401         self.assertEqual(data[0][:15], '127.0.0.1 - - [')
402         haslength = False
403         for k, v in self.headers:
404             if k.lower() == 'content-length':
405                 haslength = True
406         if haslength:
407             self.assertEqual(data[0][-42:], '] "GET /flatten/as_string HTTP/1.1" 200 7\n')
408         else:
409             self.assertEqual(data[0][-42:], '] "GET /flatten/as_string HTTP/1.1" 200 -\n')
410        
411         self.assertEqual(data[1][:15], '127.0.0.1 - - [')
412         haslength = False
413         for k, v in self.headers:
414             if k.lower() == 'content-length':
415                 haslength = True
416         if haslength:
417             self.assertEqual(data[1][-41:], '] "GET /flatten/as_yield HTTP/1.1" 200 7\n')
418         else:
419             self.assertEqual(data[1][-41:], '] "GET /flatten/as_yield HTTP/1.1" 200 -\n')
420        
421         data = open(logFile, "rb").readlines()
422         self.assertEqual(data, [])
423        
424         # Test that error log gets access messages if no logAccess defined.
425         self.getPage("/params/?thing=a")
426         self.assertBody("'a'")
427         data = open(logFile, "rb").readlines()
428         self.assertEqual(data[0][-53:], ' HTTP INFO 127.0.0.1 - GET /params/?thing=a HTTP/1.1\n')
429        
430         # Test that tracebacks get written to the error log.
431         ignore = helper.webtest.ignored_exceptions
432         ignore.append(ValueError)
433         try:
434             self.getPage("/error/page_method")
435             self.assertInBody("raise ValueError()")
436             data = open(logFile, "rb").readlines()
437             self.assertEqual(data[2][-41:], ' INFO Traceback (most recent call last):\n')
438             self.assertEqual(data[8], '    raise ValueError()\n')
439         finally:
440             ignore.pop()
441    
442     def testRedirect(self):
443         self.getPage("/redirect/")
444         self.assertBody('child')
445         self.assertStatus('200 OK')
446        
447         self.getPage("/redirect?id=3")
448         self.assert_(self.status in ('302 Found', '303 See Other'))
449         self.assertInBody("<a href='http://127.0.0.1:%s/redirect/?id=3'>"
450                           "http://127.0.0.1:%s/redirect/?id=3</a>" %
451                           (self.PORT, self.PORT))
452        
453         self.getPage("/redirect/by_code?code=300")
454         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
455         self.assertStatus('300 Multiple Choices')
456        
457         self.getPage("/redirect/by_code?code=301")
458         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
459         self.assertStatus('301 Moved Permanently')
460        
461         self.getPage("/redirect/by_code?code=302")
462         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
463         self.assertStatus('302 Found')
464        
465         self.getPage("/redirect/by_code?code=303")
466         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
467         self.assertStatus('303 See Other')
468        
469         self.getPage("/redirect/by_code?code=307")
470         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
471         self.assertStatus('307 Temporary Redirect')
472        
473         self.getPage("/redirect/nomodify")
474         self.assertBody('')
475         self.assertStatus('304 Not modified')
476        
477         self.getPage("/redirect/proxy")
478         self.assertBody('')
479         self.assertStatus('305 Use Proxy')
480        
481         # InternalRedirect
482         self.getPage("/redirect/internal")
483         self.assertBody('hello')
484         self.assertStatus('200 OK')
485        
486         self.getPage("/redirect/internal2?user_id=Sir-not-appearing-in-this-film")
487         self.assertBody('0 images for Sir-not-appearing-in-this-film')
488         self.assertStatus('200 OK')
489        
490         self.getPage("/redirect/internal2?user_id=parrot")
491         self.assertBody('0 images for slug')
492         self.assertStatus('200 OK')
493        
494         self.getPage("/redirect/internal2?user_id=terrier")
495         self.assertBody('0 images for fish')
496         self.assertStatus('200 OK')
497        
498         # HTTPRedirect on error
499         self.getPage("/redirect/error")
500         self.assertStatus('303 See Other')
501         self.assertInBody('/errpage')
502    
503     def testFlatten(self):
504         for url in ["/flatten/as_string", "/flatten/as_list",
505                     "/flatten/as_yield", "/flatten/as_dblyield",
506                     "/flatten/as_refyield"]:
507             self.getPage(url)
508             self.assertBody('content')
509    
510     def testErrorHandling(self):
511         self.getPage("/error/missing")
512         self.assertStatus("404 Not Found")
513         self.assertErrorPage(404, "The path '/error/missing' was not found.")
514        
515         ignore = helper.webtest.ignored_exceptions
516         ignore.append(ValueError)
517         try:
518             valerr = r'\n    raise ValueError\(\)\nValueError\n'
519             self.getPage("/error/page_method")
520             self.assertErrorPage(500, pattern=valerr)
521            
522             self.getPage("/error/page_yield")
523             self.assertErrorPage(500, pattern=valerr)
524            
525             import cherrypy
526             # streamResponse should be True for this path.
527             if cherrypy.server.httpserver is None:
528                 self.assertRa