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:
parent
a0ef3ba2f7
commit
36b164d838
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue