diff --git a/django/test/client.py b/django/test/client.py index f8c2705f983..945fa132812 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -592,40 +592,49 @@ class Client(RequestFactory): are incorrect, or the user is inactive, or if the sessions framework is not available. """ - from django.contrib.auth import authenticate, login + from django.contrib.auth import authenticate user = authenticate(**credentials) if (user and user.is_active and apps.is_installed('django.contrib.sessions')): - engine = import_module(settings.SESSION_ENGINE) - - # Create a fake request to store login details. - request = HttpRequest() - - if self.session: - request.session = self.session - else: - request.session = engine.SessionStore() - login(request, user) - - # Save the session values. - request.session.save() - - # Set the cookie to represent the session. - session_cookie = settings.SESSION_COOKIE_NAME - self.cookies[session_cookie] = request.session.session_key - cookie_data = { - 'max-age': None, - 'path': '/', - 'domain': settings.SESSION_COOKIE_DOMAIN, - 'secure': settings.SESSION_COOKIE_SECURE or None, - 'expires': None, - } - self.cookies[session_cookie].update(cookie_data) - + self._login(user) return True else: return False + def force_login(self, user, backend=None): + if backend is None: + backend = settings.AUTHENTICATION_BACKENDS[0] + user.backend = backend + self._login(user) + + def _login(self, user): + from django.contrib.auth import login + engine = import_module(settings.SESSION_ENGINE) + + # Create a fake request to store login details. + request = HttpRequest() + + if self.session: + request.session = self.session + else: + request.session = engine.SessionStore() + login(request, user) + + # Save the session values. + request.session.save() + + # Set the cookie to represent the session. + session_cookie = settings.SESSION_COOKIE_NAME + self.cookies[session_cookie] = request.session.session_key + cookie_data = { + 'max-age': None, + 'path': '/', + 'domain': settings.SESSION_COOKIE_DOMAIN, + 'secure': settings.SESSION_COOKIE_SECURE or None, + 'expires': None, + } + self.cookies[session_cookie].update(cookie_data) + def logout(self): """ Removes the authenticated user's cookies and session object. diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index b80d14f9851..48c850f9a7d 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -484,6 +484,11 @@ Tests * Added the :meth:`json() ` method to test client responses to give access to the response body as JSON. +* Added the :meth:`~django.test.Client.force_login()` method to the test + client. Use this method to simulate the effect of a user logging into the + site while skipping the authentication and verification steps of + :meth:`~django.test.Client.login()`. + URLs ^^^^ diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index cee2f2c1fee..d345e9c36d1 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -312,6 +312,8 @@ failed and erroneous tests. If all the tests pass, the return code is 0. This feature is useful if you're using the test-runner script in a shell script and need to test for success or failure at that level. +.. _speeding-up-tests-auth-hashers: + Speeding up the tests --------------------- diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 2ebe98eda41..fef718c7c62 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -384,6 +384,32 @@ Use the ``django.test.Client`` class to make requests. :meth:`~django.contrib.auth.models.UserManager.create_user` helper method to create a new user with a correctly hashed password. + .. method:: Client.force_login(user, backend=None) + + .. versionadded:: 1.9 + + If your site uses Django's :doc:`authentication + system`, you can use the ``force_login()`` method + to simulate the effect of a user logging into the site. Use this method + instead of :meth:`login` when a test requires a user be logged in and + the details of how a user logged in aren't important. + + Unlike ``login()``, this method skips the authentication and + verification steps: inactive users (:attr:`is_active=False + `) are permitted to login + and the user's credentials don't need to be provided. + + The user will have its ``backend`` attribute set to the value of the + ``backend`` argument (which should be a dotted Python path string), or + to ``settings.AUTHENTICATION_BACKENDS[0]`` if a value isn't provided. + The :func:`~django.contrib.auth.authenticate` function called by + :meth:`login` normally annotates the user like this. + + This method is faster than ``login()`` since the expensive + password hashing algorithms are bypassed. Also, you can speed up + ``login()`` by :ref:`using a weaker hasher while testing + `. + .. method:: Client.logout() If your site uses Django's :doc:`authentication system`, diff --git a/tests/test_client/auth_backends.py b/tests/test_client/auth_backends.py new file mode 100644 index 00000000000..c886dce3367 --- /dev/null +++ b/tests/test_client/auth_backends.py @@ -0,0 +1,5 @@ +from django.contrib.auth.backends import ModelBackend + + +class TestClientBackend(ModelBackend): + pass diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index bef11461722..ae70f5c4949 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -364,6 +364,20 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_force_login(self): + "Request a page that is protected with @login_required" + # Get the page without logging in. Should result in 302. + response = self.client.get('/login_protected_view/') + self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/') + + # Log in + self.client.force_login(self.u1) + + # Request a page that requires a login + response = self.client.get('/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_method_login(self): "Request a page that is protected with a @login_required method" @@ -380,6 +394,20 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_method_force_login(self): + "Request a page that is protected with a @login_required method" + # Get the page without logging in. Should result in 302. + response = self.client.get('/login_protected_method_view/') + self.assertRedirects(response, '/accounts/login/?next=/login_protected_method_view/') + + # Log in + self.client.force_login(self.u1) + + # Request a page that requires a login + response = self.client.get('/login_protected_method_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_login_and_custom_redirect(self): "Request a page that is protected with @login_required(redirect_field_name='redirect_to')" @@ -396,6 +424,23 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_force_login_and_custom_redirect(self): + """ + Request a page that is protected with + @login_required(redirect_field_name='redirect_to') + """ + # Get the page without logging in. Should result in 302. + response = self.client.get('/login_protected_view_custom_redirect/') + self.assertRedirects(response, '/accounts/login/?redirect_to=/login_protected_view_custom_redirect/') + + # Log in + self.client.force_login(self.u1) + + # Request a page that requires a login + response = self.client.get('/login_protected_view_custom_redirect/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_bad_login(self): "Request a page that is protected with @login, but use bad credentials" @@ -408,6 +453,21 @@ class ClientTest(TestCase): login = self.client.login(username='inactive', password='password') self.assertFalse(login) + def test_view_with_inactive_force_login(self): + "Request a page that is protected with @login, but use an inactive login" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/login_protected_view/') + self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/') + + # Log in + self.client.force_login(self.u2) + + # Request a page that requires a login + response = self.client.get('/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'inactive') + def test_logout(self): "Request a logout after logging in" # Log in @@ -425,6 +485,47 @@ class ClientTest(TestCase): response = self.client.get('/login_protected_view/') self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/') + def test_logout_with_force_login(self): + "Request a logout after logging in" + # Log in + self.client.force_login(self.u1) + + # Request a page that requires a login + response = self.client.get('/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + # Log out + self.client.logout() + + # Request a page that requires a login + response = self.client.get('/login_protected_view/') + self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/') + + @override_settings( + AUTHENTICATION_BACKENDS=[ + 'django.contrib.auth.backends.ModelBackend', + 'test_client.auth_backends.TestClientBackend', + ], + ) + def test_force_login_with_backend(self): + """ + Request a page that is protected with @login_required when using + force_login() and passing a backend. + """ + # Get the page without logging in. Should result in 302. + response = self.client.get('/login_protected_view/') + self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/') + + # Log in + self.client.force_login(self.u1, backend='test_client.auth_backends.TestClientBackend') + self.assertEqual(self.u1.backend, 'test_client.auth_backends.TestClientBackend') + + # Request a page that requires a login + response = self.client.get('/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies") def test_logout_cookie_sessions(self): self.test_logout()