Django

Code

root/django/trunk/django/test/client.py

Revision 9465, 14.4 kB (checked in by russellm, 1 week ago)

Fixed #9585 -- Corrected code committed in [9398] that wasn't compatible with Python 2.3/2.4. Thanks to Karen Tracey for the report and fix.

  • Property svn:eol-style set to native
Line 
1 import urllib
2 from urlparse import urlparse, urlunparse
3 import sys
4 import os
5 try:
6     from cStringIO import StringIO
7 except ImportError:
8     from StringIO import StringIO
9
10 from django.conf import settings
11 from django.contrib.auth import authenticate, login
12 from django.core.handlers.base import BaseHandler
13 from django.core.handlers.wsgi import WSGIRequest
14 from django.core.signals import got_request_exception
15 from django.http import SimpleCookie, HttpRequest
16 from django.template import TemplateDoesNotExist
17 from django.test import signals
18 from django.utils.functional import curry
19 from django.utils.encoding import smart_str
20 from django.utils.http import urlencode
21 from django.utils.itercompat import is_iterable
22
23 BOUNDARY = 'BoUnDaRyStRiNg'
24 MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
25
26
27 class FakePayload(object):
28     """
29     A wrapper around StringIO that restricts what can be read since data from
30     the network can't be seeked and cannot be read outside of its content
31     length. This makes sure that views can't do anything under the test client
32     that wouldn't work in Real Life.
33     """
34     def __init__(self, content):
35         self.__content = StringIO(content)
36         self.__len = len(content)
37
38     def read(self, num_bytes=None):
39         if num_bytes is None:
40             num_bytes = self.__len or 1
41         assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data."
42         content = self.__content.read(num_bytes)
43         self.__len -= num_bytes
44         return content
45
46
47 class ClientHandler(BaseHandler):
48     """
49     A HTTP Handler that can be used for testing purposes.
50     Uses the WSGI interface to compose requests, but returns
51     the raw HttpResponse object
52     """
53     def __call__(self, environ):
54         from django.conf import settings
55         from django.core import signals
56
57         # Set up middleware if needed. We couldn't do this earlier, because
58         # settings weren't available.
59         if self._request_middleware is None:
60             self.load_middleware()
61
62         signals.request_started.send(sender=self.__class__)
63         try:
64             request = WSGIRequest(environ)
65             response = self.get_response(request)
66
67             # Apply response middleware.
68             for middleware_method in self._response_middleware:
69                 response = middleware_method(request, response)
70             response = self.apply_response_fixes(request, response)
71         finally:
72             signals.request_finished.send(sender=self.__class__)
73
74         return response
75
76 def store_rendered_templates(store, signal, sender, template, context, **kwargs):
77     """
78     Stores templates and contexts that are rendered.
79     """
80     store.setdefault('template',[]).append(template)
81     store.setdefault('context',[]).append(context)
82
83 def encode_multipart(boundary, data):
84     """
85     Encodes multipart POST data from a dictionary of form values.
86
87     The key will be used as the form data name; the value will be transmitted
88     as content. If the value is a file, the contents of the file will be sent
89     as an application/octet-stream; otherwise, str(value) will be sent.
90     """
91     lines = []
92     to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
93
94     # Not by any means perfect, but good enough for our purposes.
95     is_file = lambda thing: hasattr(thing, "read") and callable(thing.read)
96
97     # Each bit of the multipart form data could be either a form value or a
98     # file, or a *list* of form values and/or files. Remember that HTTP field
99     # names can be duplicated!
100     for (key, value) in data.items():
101         if is_file(value):
102             lines.extend(encode_file(boundary, key, value))
103         elif not isinstance(value, basestring) and is_iterable(value):
104             for item in value:
105                 if is_file(item):
106                     lines.extend(encode_file(boundary, key, item))
107                 else:
108                     lines.extend([
109                         '--' + boundary,
110                         'Content-Disposition: form-data; name="%s"' % to_str(key),
111                         '',
112                         to_str(item)
113                     ])
114         else:
115             lines.extend([
116                 '--' + boundary,
117                 'Content-Disposition: form-data; name="%s"' % to_str(key),
118                 '',
119                 to_str(value)
120             ])
121
122     lines.extend([
123         '--' + boundary + '--',
124         '',
125     ])
126     return '\r\n'.join(lines)
127
128 def encode_file(boundary, key, file):
129     to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
130     return [
131         '--' + boundary,
132         'Content-Disposition: form-data; name="%s"; filename="%s"' \
133             % (to_str(key), to_str(os.path.basename(file.name))),
134         'Content-Type: application/octet-stream',
135         '',
136         file.read()
137     ]
138
139 class Client(object):
140     """
141     A class that can act as a client for testing purposes.
142
143     It allows the user to compose GET and POST requests, and
144     obtain the response that the server gave to those requests.
145     The server Response objects are annotated with the details
146     of the contexts and templates that were rendered during the
147     process of serving the request.
148
149     Client objects are stateful - they will retain cookie (and
150     thus session) details for the lifetime of the Client instance.
151
152     This is not intended as a replacement for Twill/Selenium or
153     the like - it is here to allow testing against the
154     contexts and templates produced by a view, rather than the
155     HTML rendered to the end-user.
156     """
157     def __init__(self, **defaults):
158         self.handler = ClientHandler()
159         self.defaults = defaults
160         self.cookies = SimpleCookie()
161         self.exc_info = None
162         self.errors = StringIO()
163
164     def store_exc_info(self, **kwargs):
165         """
166         Stores exceptions when they are generated by a view.
167         """
168         self.exc_info = sys.exc_info()
169
170     def _session(self):
171         """
172         Obtains the current session variables.
173         """
174         if 'django.contrib.sessions' in settings.INSTALLED_APPS:
175             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
176             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
177             if cookie:
178                 return engine.SessionStore(cookie.value)
179         return {}
180     session = property(_session)
181
182     def request(self, **request):
183         """
184         The master request method. Composes the environment dictionary
185         and passes to the handler, returning the result of the handler.
186         Assumes defaults for the query environment, which can be overridden
187         using the arguments to the request.
188         """
189         environ = {
190             'HTTP_COOKIE':      self.cookies,
191             'PATH_INFO':         '/',
192             'QUERY_STRING':      '',
193             'REQUEST_METHOD':    'GET',
194             'SCRIPT_NAME':       '',
195             'SERVER_NAME':       'testserver',
196             'SERVER_PORT':       '80',
197             'SERVER_PROTOCOL':   'HTTP/1.1',
198             'wsgi.version':      (1,0),
199             'wsgi.url_scheme':   'http',
200             'wsgi.errors':       self.errors,
201             'wsgi.multiprocess': True,
202             'wsgi.multithread':  False,
203             'wsgi.run_once':     False,
204         }
205         environ.update(self.defaults)
206         environ.update(request)
207
208         # Curry a data dictionary into an instance of the template renderer
209         # callback function.
210         data = {}
211         on_template_render = curry(store_rendered_templates, data)
212         signals.template_rendered.connect(on_template_render)
213
214         # Capture exceptions created by the handler.
215         got_request_exception.connect(self.store_exc_info)
216
217         try:
218             response = self.handler(environ)
219         except TemplateDoesNotExist, e:
220             # If the view raises an exception, Django will attempt to show
221             # the 500.html template. If that template is not available,
222             # we should ignore the error in favor of re-raising the
223             # underlying exception that caused the 500 error. Any other
224             # template found to be missing during view error handling
225             # should be reported as-is.
226             if e.args != ('500.html',):
227                 raise
228
229         # Look for a signalled exception, clear the current context
230         # exception data, then re-raise the signalled exception.
231         # Also make sure that the signalled exception is cleared from
232         # the local cache!
233         if self.exc_info:
234             exc_info = self.exc_info
235             self.exc_info = None
236             raise exc_info[1], None, exc_info[2]
237
238         # Save the client and request that stimulated the response.
239         response.client = self
240         response.request = request
241
242         # Add any rendered template detail to the response.
243         # If there was only one template rendered (the most likely case),
244         # flatten the list to a single element.
245         for detail in ('template', 'context'):
246             if data.get(detail):
247                 if len(data[detail]) == 1:
248                     setattr(response, detail, data[detail][0]);
249                 else:
250                     setattr(response, detail, data[detail])
251             else:
252                 setattr(response, detail, None)
253
254         # Update persistent cookie data.
255         if response.cookies:
256             self.cookies.update(response.cookies)
257
258         return response
259
260     def get(self, path, data={}, **extra):
261         """
262         Requests a response from the server using GET.
263         """
264         parsed = urlparse(path)
265         r = {
266             'CONTENT_TYPE':    'text/html; charset=utf-8',
267             'PATH_INFO':       urllib.unquote(parsed[2]),
268             'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
269             'REQUEST_METHOD': 'GET',
270             'wsgi.input':      FakePayload('')
271         }
272         r.update(extra)
273
274         return self.request(**r)
275
276     def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
277         """
278         Requests a response from the server using POST.
279         """
280         if content_type is MULTIPART_CONTENT:
281             post_data = encode_multipart(BOUNDARY, data)
282         else:
283             post_data = data
284
285         parsed = urlparse(path)
286         r = {
287             'CONTENT_LENGTH': len(post_data),
288             'CONTENT_TYPE':   content_type,
289             'PATH_INFO':      urllib.unquote(parsed[2]),
290             'QUERY_STRING':   parsed[4],
291             'REQUEST_METHOD': 'POST',
292             'wsgi.input':     FakePayload(post_data),
293         }
294         r.update(extra)
295
296         return self.request(**r)
297
298     def head(self, path, data={}, **extra):
299         """
300         Request a response from the server using HEAD.
301         """
302         parsed = urlparse(path)
303         r = {
304             'CONTENT_TYPE':    'text/html; charset=utf-8',
305             'PATH_INFO':       urllib.unquote(parsed[2]),
306             'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
307             'REQUEST_METHOD': 'HEAD',
308             'wsgi.input':      FakePayload('')
309         }
310         r.update(extra)
311
312         return self.request(**r)
313
314     def options(self, path, data={}, **extra):
315         """
316         Request a response from the server using OPTIONS.
317         """
318         parsed = urlparse(path)
319         r = {
320             'PATH_INFO':       urllib.unquote(parsed[2]),
321             'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
322             'REQUEST_METHOD': 'OPTIONS',
323             'wsgi.input':      FakePayload('')
324         }
325         r.update(extra)
326
327         return self.request(**r)
328
329     def put(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
330         """
331         Send a resource to the server using PUT.
332         """
333         if content_type is MULTIPART_CONTENT:
334             post_data = encode_multipart(BOUNDARY, data)
335         else:
336             post_data = data
337
338         parsed = urlparse(path)
339         r = {
340             'CONTENT_LENGTH': len(post_data),
341             'CONTENT_TYPE':   content_type,
342             'PATH_INFO':      urllib.unquote(parsed[2]),
343             'QUERY_STRING':   urlencode(data, doseq=True) or parsed[4],
344             'REQUEST_METHOD': 'PUT',
345             'wsgi.input':     FakePayload(post_data),
346         }
347         r.update(extra)
348
349         return self.request(**r)
350
351     def delete(self, path, data={}, **extra):
352         """
353         Send a DELETE request to the server.
354         """
355         parsed = urlparse(path)
356         r = {
357             'PATH_INFO':       urllib.unquote(parsed[2]),
358             'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
359             'REQUEST_METHOD': 'DELETE',
360             'wsgi.input':      FakePayload('')
361         }
362         r.update(extra)
363
364         return self.request(**r)
365
366     def login(self, **credentials):
367         """
368         Sets the Client to appear as if it has successfully logged into a site.
369
370         Returns True if login is possible; False if the provided credentials
371         are incorrect, or the user is inactive, or if the sessions framework is
372         not available.
373         """
374         user = authenticate(**credentials)
375         if user and user.is_active \
376                 and 'django.contrib.sessions' in settings.INSTALLED_APPS:
377             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
378
379             # Create a fake request to store login details.
380             request = HttpRequest()
381             if self.session:
382                 request.session = self.session
383             else:
384                 request.session = engine.SessionStore()
385             login(request, user)
386
387             # Set the cookie to represent the session.
388             session_cookie = settings.SESSION_COOKIE_NAME
389             self.cookies[session_cookie] = request.session.session_key
390             cookie_data = {
391                 'max-age': None,
392                 'path': '/',
393                 'domain': settings.SESSION_COOKIE_DOMAIN,
394                 'secure': settings.SESSION_COOKIE_SECURE or None,
395                 'expires': None,
396             }
397             self.cookies[session_cookie].update(cookie_data)
398
399             # Save the session values.
400             request.session.save()
401
402             return True
403         else:
404             return False
405
406     def logout(self):
407         """
408         Removes the authenticated user's cookies.
409
410         Causes the authenticated user to be logged out.
411         """
412         session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
413         session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
414         self.cookies = SimpleCookie()
Note: See TracBrowser for help on using the browser.