Browse Source

Fixed #20916 -- Added Client.force_login() to bypass authentication.

Jon Dufresne 9 years ago
parent
commit
b44dee16e6

+ 35 - 26
django/test/client.py

@@ -592,39 +592,48 @@ 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)
+            self._login(user)
+            return True
+        else:
+            return False
 
-            # Create a fake request to store login details.
-            request = HttpRequest()
+    def force_login(self, user, backend=None):
+        if backend is None:
+            backend = settings.AUTHENTICATION_BACKENDS[0]
+        user.backend = backend
+        self._login(user)
 
-            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 _login(self, user):
+        from django.contrib.auth import login
+        engine = import_module(settings.SESSION_ENGINE)
 
-            return True
+        # Create a fake request to store login details.
+        request = HttpRequest()
+
+        if self.session:
+            request.session = self.session
         else:
-            return False
+            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):
         """

+ 5 - 0
docs/releases/1.9.txt

@@ -484,6 +484,11 @@ Tests
 * Added the :meth:`json() <django.test.Response.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
 ^^^^
 

+ 2 - 0
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
 ---------------------
 

+ 26 - 0
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</topics/auth/index>`, 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
+        <django.contrib.auth.models.User.is_active>`) 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
+        <speeding-up-tests-auth-hashers>`.
+
     .. method:: Client.logout()
 
         If your site uses Django's :doc:`authentication system</topics/auth/index>`,

+ 5 - 0
tests/test_client/auth_backends.py

@@ -0,0 +1,5 @@
+from django.contrib.auth.backends import ModelBackend
+
+
+class TestClientBackend(ModelBackend):
+    pass

+ 101 - 0
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()