app/django/test/client.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     1 import urllib
       
     2 import sys
       
     3 from cStringIO import StringIO
       
     4 from django.conf import settings
       
     5 from django.contrib.auth import authenticate, login
       
     6 from django.core.handlers.base import BaseHandler
       
     7 from django.core.handlers.wsgi import WSGIRequest
       
     8 from django.core.signals import got_request_exception
       
     9 from django.dispatch import dispatcher
       
    10 from django.http import SimpleCookie, HttpRequest
       
    11 from django.template import TemplateDoesNotExist
       
    12 from django.test import signals
       
    13 from django.utils.functional import curry
       
    14 from django.utils.encoding import smart_str
       
    15 from django.utils.http import urlencode
       
    16 from django.utils.itercompat import is_iterable
       
    17 
       
    18 BOUNDARY = 'BoUnDaRyStRiNg'
       
    19 MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
       
    20 
       
    21 class ClientHandler(BaseHandler):
       
    22     """
       
    23     A HTTP Handler that can be used for testing purposes.
       
    24     Uses the WSGI interface to compose requests, but returns
       
    25     the raw HttpResponse object
       
    26     """
       
    27     def __call__(self, environ):
       
    28         from django.conf import settings
       
    29         from django.core import signals
       
    30 
       
    31         # Set up middleware if needed. We couldn't do this earlier, because
       
    32         # settings weren't available.
       
    33         if self._request_middleware is None:
       
    34             self.load_middleware()
       
    35 
       
    36         dispatcher.send(signal=signals.request_started)
       
    37         try:
       
    38             request = WSGIRequest(environ)
       
    39             response = self.get_response(request)
       
    40 
       
    41             # Apply response middleware
       
    42             for middleware_method in self._response_middleware:
       
    43                 response = middleware_method(request, response)
       
    44             response = self.apply_response_fixes(request, response)
       
    45         finally:
       
    46             dispatcher.send(signal=signals.request_finished)
       
    47 
       
    48         return response
       
    49 
       
    50 def store_rendered_templates(store, signal, sender, template, context):
       
    51     "A utility function for storing templates and contexts that are rendered"
       
    52     store.setdefault('template',[]).append(template)
       
    53     store.setdefault('context',[]).append(context)
       
    54 
       
    55 def encode_multipart(boundary, data):
       
    56     """
       
    57     A simple method for encoding multipart POST data from a dictionary of
       
    58     form values.
       
    59 
       
    60     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
       
    62     as an application/octet-stream; otherwise, str(value) will be sent.
       
    63     """
       
    64     lines = []
       
    65     to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
       
    66     for (key, value) in data.items():
       
    67         if isinstance(value, file):
       
    68             lines.extend([
       
    69                 '--' + boundary,
       
    70                 'Content-Disposition: form-data; name="%s"; filename="%s"' % (to_str(key), to_str(value.name)),
       
    71                 'Content-Type: application/octet-stream',
       
    72                 '',
       
    73                 value.read()
       
    74             ])
       
    75         else:
       
    76             if not isinstance(value, basestring) and is_iterable(value):
       
    77                 for item in value:
       
    78                     lines.extend([
       
    79                         '--' + boundary,
       
    80                         'Content-Disposition: form-data; name="%s"' % to_str(key),
       
    81                         '',
       
    82                         to_str(item)
       
    83                     ])
       
    84             else:
       
    85                 lines.extend([
       
    86                     '--' + boundary,
       
    87                     'Content-Disposition: form-data; name="%s"' % to_str(key),
       
    88                     '',
       
    89                     to_str(value)
       
    90                 ])
       
    91 
       
    92     lines.extend([
       
    93         '--' + boundary + '--',
       
    94         '',
       
    95     ])
       
    96     return '\r\n'.join(lines)
       
    97 
       
    98 class Client:
       
    99     """
       
   100     A class that can act as a client for testing purposes.
       
   101 
       
   102     It allows the user to compose GET and POST requests, and
       
   103     obtain the response that the server gave to those requests.
       
   104     The server Response objects are annotated with the details
       
   105     of the contexts and templates that were rendered during the
       
   106     process of serving the request.
       
   107 
       
   108     Client objects are stateful - they will retain cookie (and
       
   109     thus session) details for the lifetime of the Client instance.
       
   110 
       
   111     This is not intended as a replacement for Twill/Selenium or
       
   112     the like - it is here to allow testing against the
       
   113     contexts and templates produced by a view, rather than the
       
   114     HTML rendered to the end-user.
       
   115     """
       
   116     def __init__(self, **defaults):
       
   117         self.handler = ClientHandler()
       
   118         self.defaults = defaults
       
   119         self.cookies = SimpleCookie()
       
   120         self.exc_info = None
       
   121 
       
   122     def store_exc_info(self, *args, **kwargs):
       
   123         """
       
   124         Utility method that can be used to store exceptions when they are
       
   125         generated by a view.
       
   126         """
       
   127         self.exc_info = sys.exc_info()
       
   128 
       
   129     def _session(self):
       
   130         "Obtain the current session variables"
       
   131         if 'django.contrib.sessions' in settings.INSTALLED_APPS:
       
   132             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
       
   133             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
       
   134             if cookie:
       
   135                 return engine.SessionStore(cookie.value)
       
   136         return {}
       
   137     session = property(_session)
       
   138 
       
   139     def request(self, **request):
       
   140         """
       
   141         The master request method. Composes the environment dictionary
       
   142         and passes to the handler, returning the result of the handler.
       
   143         Assumes defaults for the query environment, which can be overridden
       
   144         using the arguments to the request.
       
   145         """
       
   146 
       
   147         environ = {
       
   148             'HTTP_COOKIE':      self.cookies,
       
   149             'PATH_INFO':         '/',
       
   150             'QUERY_STRING':      '',
       
   151             'REQUEST_METHOD':    'GET',
       
   152             'SCRIPT_NAME':       None,
       
   153             'SERVER_NAME':       'testserver',
       
   154             'SERVER_PORT':       80,
       
   155             'SERVER_PROTOCOL':   'HTTP/1.1',
       
   156         }
       
   157         environ.update(self.defaults)
       
   158         environ.update(request)
       
   159 
       
   160         # Curry a data dictionary into an instance of
       
   161         # the template renderer callback function
       
   162         data = {}
       
   163         on_template_render = curry(store_rendered_templates, data)
       
   164         dispatcher.connect(on_template_render, signal=signals.template_rendered)
       
   165 
       
   166         # Capture exceptions created by the handler
       
   167         dispatcher.connect(self.store_exc_info, signal=got_request_exception)
       
   168 
       
   169         try:
       
   170             response = self.handler(environ)
       
   171         except TemplateDoesNotExist, e:
       
   172             # If the view raises an exception, Django will attempt to show
       
   173             # the 500.html template. If that template is not available,
       
   174             # we should ignore the error in favor of re-raising the
       
   175             # underlying exception that caused the 500 error. Any other
       
   176             # template found to be missing during view error handling
       
   177             # should be reported as-is.
       
   178             if e.args != ('500.html',):
       
   179                 raise
       
   180 
       
   181         # Look for a signalled exception and reraise it
       
   182         if self.exc_info:
       
   183             raise self.exc_info[1], None, self.exc_info[2]
       
   184 
       
   185         # Save the client and request that stimulated the response
       
   186         response.client = self
       
   187         response.request = request
       
   188 
       
   189         # Add any rendered template detail to the response
       
   190         # If there was only one template rendered (the most likely case),
       
   191         # flatten the list to a single element
       
   192         for detail in ('template', 'context'):
       
   193             if data.get(detail):
       
   194                 if len(data[detail]) == 1:
       
   195                     setattr(response, detail, data[detail][0]);
       
   196                 else:
       
   197                     setattr(response, detail, data[detail])
       
   198             else:
       
   199                 setattr(response, detail, None)
       
   200 
       
   201         # Update persistent cookie data
       
   202         if response.cookies:
       
   203             self.cookies.update(response.cookies)
       
   204 
       
   205         return response
       
   206 
       
   207     def get(self, path, data={}, **extra):
       
   208         "Request a response from the server using GET."
       
   209         r = {
       
   210             'CONTENT_LENGTH':  None,
       
   211             'CONTENT_TYPE':    'text/html; charset=utf-8',
       
   212             'PATH_INFO':       urllib.unquote(path),
       
   213             'QUERY_STRING':    urlencode(data, doseq=True),
       
   214             'REQUEST_METHOD': 'GET',
       
   215         }
       
   216         r.update(extra)
       
   217 
       
   218         return self.request(**r)
       
   219 
       
   220     def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
       
   221         "Request a response from the server using POST."
       
   222 
       
   223         if content_type is MULTIPART_CONTENT:
       
   224             post_data = encode_multipart(BOUNDARY, data)
       
   225         else:
       
   226             post_data = data
       
   227 
       
   228         r = {
       
   229             'CONTENT_LENGTH': len(post_data),
       
   230             'CONTENT_TYPE':   content_type,
       
   231             'PATH_INFO':      urllib.unquote(path),
       
   232             'REQUEST_METHOD': 'POST',
       
   233             'wsgi.input':     StringIO(post_data),
       
   234         }
       
   235         r.update(extra)
       
   236 
       
   237         return self.request(**r)
       
   238 
       
   239     def login(self, **credentials):
       
   240         """Set the Client to appear as if it has sucessfully logged into a site.
       
   241 
       
   242         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
       
   244         not available.
       
   245         """
       
   246         user = authenticate(**credentials)
       
   247         if user and user.is_active and 'django.contrib.sessions' in settings.INSTALLED_APPS:
       
   248             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
       
   249 
       
   250             # Create a fake request to store login details
       
   251             request = HttpRequest()
       
   252             request.session = engine.SessionStore()
       
   253             login(request, user)
       
   254 
       
   255             # Set the cookie to represent the session
       
   256             self.cookies[settings.SESSION_COOKIE_NAME] = request.session.session_key
       
   257             self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None
       
   258             self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/'
       
   259             self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN
       
   260             self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None
       
   261             self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None
       
   262 
       
   263             # Save the session values
       
   264             request.session.save()
       
   265 
       
   266             return True
       
   267         else:
       
   268             return False
       
   269 
       
   270     def logout(self):
       
   271         """Removes the authenticated user's cookies.
       
   272 
       
   273         Causes the authenticated user to be logged out.
       
   274         """
       
   275         session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
       
   276         session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
       
   277         self.cookies = SimpleCookie()