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 import sys
from cStringIO import StringIO from cStringIO import StringIO
from urlparse import urlparse from urlparse import urlparse
from django.conf import settings 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.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.core.signals import got_request_exception from django.core.signals import got_request_exception
from django.dispatch import dispatcher 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.test import signals
from django.utils.functional import curry from django.utils.functional import curry
@ -113,7 +117,6 @@ class Client:
self.handler = ClientHandler() self.handler = ClientHandler()
self.defaults = defaults self.defaults = defaults
self.cookies = SimpleCookie() self.cookies = SimpleCookie()
self.session = {}
self.exc_info = None self.exc_info = None
def store_exc_info(self, *args, **kwargs): def store_exc_info(self, *args, **kwargs):
@ -123,6 +126,15 @@ class Client:
""" """
self.exc_info = sys.exc_info() 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): def request(self, **request):
""" """
The master request method. Composes the environment dictionary The master request method. Composes the environment dictionary
@ -171,16 +183,10 @@ class Client:
if self.exc_info: if self.exc_info:
raise self.exc_info[1], None, self.exc_info[2] raise self.exc_info[1], None, self.exc_info[2]
# Update persistent cookie and session data # Update persistent cookie data
if response.cookies: if response.cookies:
self.cookies.update(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 return response
def get(self, path, data={}, **extra): def get(self, path, data={}, **extra):
@ -215,42 +221,34 @@ class Client:
return self.request(**r) 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 user = authenticate(**credentials)
is protected by a @login_required access decorator. 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 # Set the cookie to represent the session
login is complete. Returns False if login process failed. self.cookies[settings.SESSION_COOKIE_NAME] = obj.session_key
""" self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None
# First, GET the page that is login protected. self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/'
# This page will redirect to the login page. self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN
response = self.get(path) self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None
if response.status_code != 302: 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 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 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. need to manually close the file after it has been provided to the POST.
``login(path, username, password)`` ``login(**credentials)``
In a production site, it is likely that some views will be protected with ** New in Django development version **
the @login_required decorator provided by ``django.contrib.auth``. Interacting
with a URL that has been login protected is a slightly complex operation, On a production site, it is likely that some views will be protected from
so the Test Client provides a simple method to automate the login process. A anonymous access through the use of the @login_required decorator, or some
call to ``login()`` stimulates the series of GET and POST calls required other login checking mechanism. The ``login()`` method can be used to
to log a user into a @login_required protected view. 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
If login is possible, the final return value of ``login()`` is the response to pass any login-based tests that may form part of a view.
that is generated by issuing a GET request on the protected URL. If login
is not possible, ``login()`` returns False. 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, 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 which contains no users by default. As a result, logins that are valid
site will not work. You will need to create users as part of the test suite on your production site will not work under test conditions. You will
to be able to test logins to your application. need to create users as part of the test suite (either manually, or
using a test fixture).
Testing Responses Testing Responses
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~

View File

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