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