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

This commit is contained in:
Jon Dufresne 2015-06-15 21:35:19 -07:00 committed by Tim Graham
parent 39ec59d6d0
commit b44dee16e6
6 changed files with 175 additions and 27 deletions

View File

@ -592,40 +592,49 @@ 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)
# 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)
return True return True
else: else:
return False 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): def logout(self):
""" """
Removes the authenticated user's cookies and session object. Removes the authenticated user's cookies and session object.

View File

@ -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
^^^^ ^^^^

View File

@ -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
--------------------- ---------------------

View File

@ -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>`,

View File

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

View File

@ -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()