Thursday, September 2, 2010

Using HTTPS inside Django unit tests

The Django documents at http://docs.djangoproject.com/en/dev/topics/testing/ discuss how to write unit tests for verifying the is_ajax() command:
The extra keyword arguments parameter can be used to specify headers to be sent in the request. For example:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
...       HTTP_X_REQUESTED_WITH='XMLHttpRequest')
...will send the HTTP header HTTP_X_REQUESTED_WITH to the details view, which is a good way to test code paths that use the django.http.HttpRequest.is_ajax() method.
But what about testing for HTTPS connections? (i.e. HttpRequest.is_secure())

The key to figuring out this issue is to dig into django.test.client.py code, where self.client.get() and self.client.post() are defined. You will notice both of these routines will both ultimately call self.request():
def get(self, path, data={}, follow=False, **extra):
        """                                                                                                                                                             
        Requests a response from the server using GET.                                                                                                                  
        """
        r = {
            'CONTENT_TYPE':    'text/html; charset=utf-8',
            'PATH_INFO':       urllib.unquote(parsed[2]),
            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
            'REQUEST_METHOD': 'GET',
            'wsgi.input':      FakePayload('')
        }
        r.update(extra)

        response = self.request(**r)

    def post(self, path, data={}, content_type=MULTIPART_CONTENT,
             follow=False, **extra):
        """                                                                          
        Requests a response from the server using POST.                                                                                                                 
        """
.
.
.
        response = self.request(**r)

    def request(self, **request):
        """                                                                                                                                                             
        The master request method. Composes the environment dictionary                                                                                                  
        and passes to the handler, returning the result of the handler.                                                                                                 
        Assumes defaults for the query environment, which can be overridden                                                                                             
        using the arguments to the request.                                                                                                                             
        """
        environ = {
            'HTTP_COOKIE':       self.cookies.output(header='', sep='; '),
            'PATH_INFO':         '/',
            'QUERY_STRING':      '',
            'REMOTE_ADDR':       '127.0.0.1',
            'REQUEST_METHOD':    'GET',
            'SCRIPT_NAME':       '',
            'SERVER_NAME':       'testserver',
            'SERVER_PORT':       '80',
            'SERVER_PROTOCOL':   'HTTP/1.1',
            'wsgi.version':      (1,0),
            'wsgi.url_scheme':   'http',
            'wsgi.errors':       self.errors,
            'wsgi.multiprocess': True,
            'wsgi.multithread':  False,
            'wsgi.run_once':     False,
        }
        environ.update(self.defaults)
        environ.update(request)
Because the arguments **request originally came from the **extra parameters, we can override wsgi.url_scheme to be "https":
kwargs = {}
        kwargs["wsgi.url_scheme"] = "https"
        kwargs["HTTP_X_REQUESTED_WITH"] = 'XMLHttpRequest'

        response1 = self.client.post('%s' % my_url,
                                     {'test1' : '123456'}
                                     **kwargs)
Why does overriding the wsgi.url_scheme variable work? If you use the Python debugger and put an pdb.set_trace() inside one of your view handlers, you will notice the Django unit test runner uses django.core.handlers.wsgi.WSGIRequest for its request object (and not django.http.HttpRequest). We can then look inside django.core.handlers.wsgi.WSGIRequest to see how the is_secure() function is used:
def is_secure(self):
        return 'wsgi.url_scheme' in self.environ \
            and self.environ['wsgi.url_scheme'] == 'https'
So by overriding the wsgi.url_scheme variable, we can force our application to mimic the behavior when HTTPS is used.

For more on writing basic unit tests in Django, see this article link too: http://dougalmatthews.com/articles/2010/jan/20/testing-your-first-django-app/

1 comment: