diff --git a/django/test/client.py b/django/test/client.py index 95d3b85922..c3110f02ec 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -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) + \ No newline at end of file diff --git a/docs/testing.txt b/docs/testing.txt index 5a2579b624..2133df797f 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -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. - - 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. +``login(**credentials)`` + ** New in Django development version ** + + 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 ~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 58f1d47e50..bad0948291 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -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"