Backwards incompatible change: Changed the way test.Client.login operates. Old implemenation was fragile, and tightly bound to forms. New implementation interfaces directly with the login system, is compatible with any authentication backend, and doesn't depend upon specific template inputs being available.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5152 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2007-05-05 15:16:15 +00:00
parent a0ef3ba2f7
commit 36b164d838
3 changed files with 75 additions and 63 deletions

View File

@ -1,12 +1,16 @@
import datetime
import sys
from cStringIO import StringIO
from urlparse import urlparse
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionWrapper
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.core.signals import got_request_exception
from django.dispatch import dispatcher
from django.http import urlencode, SimpleCookie
from django.http import urlencode, SimpleCookie, HttpRequest
from django.test import signals
from django.utils.functional import curry
@ -113,7 +117,6 @@ class Client:
self.handler = ClientHandler()
self.defaults = defaults
self.cookies = SimpleCookie()
self.session = {}
self.exc_info = None
def store_exc_info(self, *args, **kwargs):
@ -123,6 +126,15 @@ class Client:
"""
self.exc_info = sys.exc_info()
def _session(self):
"Obtain the current session variables"
if 'django.contrib.sessions' in settings.INSTALLED_APPS:
cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
if cookie:
return SessionWrapper(cookie.value)
return {}
session = property(_session)
def request(self, **request):
"""
The master request method. Composes the environment dictionary
@ -171,16 +183,10 @@ class Client:
if self.exc_info:
raise self.exc_info[1], None, self.exc_info[2]
# Update persistent cookie and session data
# Update persistent cookie data
if response.cookies:
self.cookies.update(response.cookies)
if 'django.contrib.sessions' in settings.INSTALLED_APPS:
from django.contrib.sessions.middleware import SessionWrapper
cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
if cookie:
self.session = SessionWrapper(cookie.value)
return response
def get(self, path, data={}, **extra):
@ -215,42 +221,34 @@ class Client:
return self.request(**r)
def login(self, path, username, password, **extra):
def login(self, **credentials):
"""Set the Client to appear as if it has sucessfully logged into a site.
Returns True if login is possible; False if the provided credentials
are incorrect, or if the Sessions framework is not available.
"""
A specialized sequence of GET and POST to log into a view that
is protected by a @login_required access decorator.
user = authenticate(**credentials)
if user and 'django.contrib.sessions' in settings.INSTALLED_APPS:
obj = Session.objects.get_new_session_object()
path should be the URL of the page that is login protected.
# Create a fake request to store login details
request = HttpRequest()
request.session = SessionWrapper(obj.session_key)
login(request, user)
Returns the response from GETting the requested URL after
login is complete. Returns False if login process failed.
"""
# First, GET the page that is login protected.
# This page will redirect to the login page.
response = self.get(path)
if response.status_code != 302:
# Set the cookie to represent the session
self.cookies[settings.SESSION_COOKIE_NAME] = obj.session_key
self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None
self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/'
self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN
self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None
self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None
# Set the session values
Session.objects.save(obj.session_key, request.session._session,
datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
return True
else:
return False
_, _, login_path, _, data, _= urlparse(response['Location'])
next = data.split('=')[1]
# Second, GET the login page; required to set up cookies
response = self.get(login_path, **extra)
if response.status_code != 200:
return False
# Last, POST the login data.
form_data = {
'username': username,
'password': password,
'next' : next,
}
response = self.post(login_path, data=form_data, **extra)
# Login page should 302 redirect to the originally requested page
if (response.status_code != 302 or
urlparse(response['Location'])[2] != path):
return False
# Since we are logged in, request the actual page again
return self.get(path)

View File

@ -246,22 +246,35 @@ can be invoked on the ``Client`` instance.
file name), and `attachment_file` (containing the file data). Note that you
need to manually close the file after it has been provided to the POST.
``login(path, username, password)``
In a production site, it is likely that some views will be protected with
the @login_required decorator provided by ``django.contrib.auth``. Interacting
with a URL that has been login protected is a slightly complex operation,
so the Test Client provides a simple method to automate the login process. A
call to ``login()`` stimulates the series of GET and POST calls required
to log a user into a @login_required protected view.
``login(**credentials)``
** New in Django development version **
If login is possible, the final return value of ``login()`` is the response
that is generated by issuing a GET request on the protected URL. If login
is not possible, ``login()`` returns False.
On a production site, it is likely that some views will be protected from
anonymous access through the use of the @login_required decorator, or some
other login checking mechanism. The ``login()`` method can be used to
simulate the effect of a user logging into the site. As a result of calling
this method, the Client will have all the cookies and session data required
to pass any login-based tests that may form part of a view.
In most cases, the ``credentials`` required by this method are the username
and password of the user that wants to log in, provided as keyword
arguments::
c = Client()
c.login(username='fred', password='secret')
# Now you can access a login protected view
If you are using a different authentication backend, this method may
require different credentials.
``login()`` returns ``True`` if it the credentials were accepted and login
was successful.
Note that since the test suite will be executed using the test database,
which contains no users by default. As a result, logins for your production
site will not work. You will need to create users as part of the test suite
to be able to test logins to your application.
which contains no users by default. As a result, logins that are valid
on your production site will not work under test conditions. You will
need to create users as part of the test suite (either manually, or
using a test fixture).
Testing Responses
~~~~~~~~~~~~~~~~~

View File

@ -127,18 +127,19 @@ class ClientTest(TestCase):
response = self.client.get('/test_client/login_protected_view/')
self.assertRedirects(response, '/accounts/login/')
# Log in
self.client.login(username='testclient', password='password')
# Request a page that requires a login
response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
self.failUnless(response)
response = self.client.get('/test_client/login_protected_view/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['user'].username, 'testclient')
self.assertEqual(response.template.name, 'Login Template')
def test_view_with_bad_login(self):
"Request a page that is protected with @login, but use bad credentials"
response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword')
self.failIf(response)
login = self.client.login(username='otheruser', password='nopassword')
self.failIf(login)
def test_session_modifying_view(self):
"Request a page that modifies the session"