浏览代码

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

Jon Dufresne 9 年之前
父节点
当前提交
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
         are incorrect, or the user is inactive, or if the sessions framework is
         not available.
         not available.
         """
         """
-        from django.contrib.auth import authenticate, login
+        from django.contrib.auth import authenticate
         user = authenticate(**credentials)
         user = authenticate(**credentials)
         if (user and user.is_active and
         if (user and user.is_active and
                 apps.is_installed('django.contrib.sessions')):
                 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:
         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):
     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
 * Added the :meth:`json() <django.test.Response.json>` method to test client
   responses to give access to the response body as JSON.
   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
 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
 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.
 need to test for success or failure at that level.
 
 
+.. _speeding-up-tests-auth-hashers:
+
 Speeding up the tests
 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
         :meth:`~django.contrib.auth.models.UserManager.create_user` helper
         method to create a new user with a correctly hashed password.
         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()
     .. method:: Client.logout()
 
 
         If your site uses Django's :doc:`authentication system</topics/auth/index>`,
         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.status_code, 200)
         self.assertEqual(response.context['user'].username, 'testclient')
         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):
     def test_view_with_method_login(self):
         "Request a page that is protected with a @login_required method"
         "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.status_code, 200)
         self.assertEqual(response.context['user'].username, 'testclient')
         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):
     def test_view_with_login_and_custom_redirect(self):
         "Request a page that is protected with @login_required(redirect_field_name='redirect_to')"
         "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.status_code, 200)
         self.assertEqual(response.context['user'].username, 'testclient')
         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):
     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"
 
 
@@ -408,6 +453,21 @@ class ClientTest(TestCase):
         login = self.client.login(username='inactive', password='password')
         login = self.client.login(username='inactive', password='password')
         self.assertFalse(login)
         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):
     def test_logout(self):
         "Request a logout after logging in"
         "Request a logout after logging in"
         # Log in
         # Log in
@@ -425,6 +485,47 @@ class ClientTest(TestCase):
         response = self.client.get('/login_protected_view/')
         response = self.client.get('/login_protected_view/')
         self.assertRedirects(response, '/accounts/login/?next=/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")
     @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies")
     def test_logout_cookie_sessions(self):
     def test_logout_cookie_sessions(self):
         self.test_logout()
         self.test_logout()