Skip to content

Commit

Permalink
Fix #20447: URL names given to contrib.auth.views are now resolved.
Browse files Browse the repository at this point in the history
This commit also adds tests for the redirect feature of most auth views.
It also cleans up the tests, most notably using @override_settings instead
of ad-hoc setUp/tearDown methods.

Thanks to caumons for the report.

Conflicts:
	docs/releases/1.6.txt
  • Loading branch information
bmispelon authored and mjtamlyn committed May 19, 2013
1 parent 9b22bad commit 980ae2a
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 60 deletions.
214 changes: 154 additions & 60 deletions django/contrib/auth/tests/test_views.py
@@ -1,6 +1,10 @@
import itertools import itertools
import os import os
import re import re
try:
from urllib.parse import urlparse, ParseResult
except ImportError: # Python 2
from urlparse import urlparse, ParseResult


from django.conf import global_settings, settings from django.conf import global_settings, settings
from django.contrib.sites.models import Site, RequestSite from django.contrib.sites.models import Site, RequestSite
Expand Down Expand Up @@ -46,15 +50,26 @@ def login(self, password='password'):
'username': 'testclient', 'username': 'testclient',
'password': password, 'password': password,
}) })
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL))
self.assertTrue(SESSION_KEY in self.client.session) self.assertTrue(SESSION_KEY in self.client.session)
return response


def assertFormError(self, response, error): def assertFormError(self, response, error):
"""Assert that error is found in response.context['form'] errors""" """Assert that error is found in response.context['form'] errors"""
form_errors = list(itertools.chain(*response.context['form'].errors.values())) form_errors = list(itertools.chain(*response.context['form'].errors.values()))
self.assertIn(force_text(error), form_errors) self.assertIn(force_text(error), form_errors)


def assertURLEqual(self, url, expected):
"""
Given two URLs, make sure all their components (the ones given by
urlparse) are equal, only comparing components that are present in both
URLs.
"""
fields = ParseResult._fields

for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
if x and y and x != y:
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))



@skipIfCustomUser @skipIfCustomUser
class AuthViewNamedURLTests(AuthViewsTestCase): class AuthViewNamedURLTests(AuthViewsTestCase):
Expand Down Expand Up @@ -153,7 +168,6 @@ def test_poisoned_http_host_admin_site(self):
def _test_confirm_start(self): def _test_confirm_start(self):
# Start by creating the email # Start by creating the email
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0]) return self._read_signup_email(mail.outbox[0])


Expand Down Expand Up @@ -205,8 +219,6 @@ def test_confirm_complete(self):
url, path = self._test_confirm_start() url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword', response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'anewpassword'}) 'new_password2': 'anewpassword'})
# It redirects us to a 'complete' page:
self.assertEqual(response.status_code, 302)
# Check the password has been changed # Check the password has been changed
u = User.objects.get(email='staffmember@example.com') u = User.objects.get(email='staffmember@example.com')
self.assertTrue(u.check_password("anewpassword")) self.assertTrue(u.check_password("anewpassword"))
Expand All @@ -221,6 +233,47 @@ def test_confirm_different_passwords(self):
'new_password2': 'x'}) 'new_password2': 'x'})
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])


def test_reset_redirect_default(self):
response = self.client.post('/password_reset/',
{'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/done/')

def test_reset_custom_redirect(self):
response = self.client.post('/password_reset/custom_redirect/',
{'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')

def test_reset_custom_redirect_named(self):
response = self.client.post('/password_reset/custom_redirect/named/',
{'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')

def test_confirm_redirect_default(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/reset/done/')

def test_confirm_redirect_custom(self):
url, path = self._test_confirm_start()
path = path.replace('/reset/', '/reset/custom/')
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')

def test_confirm_redirect_custom_named(self):
url, path = self._test_confirm_start()
path = path.replace('/reset/', '/reset/custom/named/')
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')



@override_settings(AUTH_USER_MODEL='auth.CustomUser') @override_settings(AUTH_USER_MODEL='auth.CustomUser')
class CustomUserPasswordResetTest(AuthViewsTestCase): class CustomUserPasswordResetTest(AuthViewsTestCase):
Expand Down Expand Up @@ -285,8 +338,6 @@ def test_password_change_succeeds(self):
'new_password1': 'password1', 'new_password1': 'password1',
'new_password2': 'password1', 'new_password2': 'password1',
}) })
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/password_change/done/'))
self.fail_login() self.fail_login()
self.login(password='password1') self.login(password='password1')


Expand All @@ -298,13 +349,43 @@ def test_password_change_done_succeeds(self):
'new_password2': 'password1', 'new_password2': 'password1',
}) })
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/password_change/done/')) self.assertURLEqual(response.url, '/password_change/done/')


@override_settings(LOGIN_URL='/login/')
def test_password_change_done_fails(self): def test_password_change_done_fails(self):
with self.settings(LOGIN_URL='/login/'): response = self.client.get('/password_change/done/')
response = self.client.get('/password_change/done/') self.assertEqual(response.status_code, 302)
self.assertEqual(response.status_code, 302) self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
self.assertTrue(response.url.endswith('/login/?next=/password_change/done/'))
def test_password_change_redirect_default(self):
self.login()
response = self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_change/done/')

def test_password_change_redirect_custom(self):
self.login()
response = self.client.post('/password_change/custom/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')

def test_password_change_redirect_custom_named(self):
self.login()
response = self.client.post('/password_change/custom/named/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')




@skipIfCustomUser @skipIfCustomUser
Expand Down Expand Up @@ -374,63 +455,68 @@ def test_login_form_contains_request(self):
# the custom authentication form used by this login asserts # the custom authentication form used by this login asserts
# that a request is passed to the form successfully. # that a request is passed to the form successfully.



@skipIfCustomUser @skipIfCustomUser
class LoginURLSettings(AuthViewsTestCase): class LoginURLSettings(AuthViewsTestCase):

"""Tests for settings.LOGIN_URL."""
def setUp(self): def assertLoginURLEquals(self, url):
super(LoginURLSettings, self).setUp()
self.old_LOGIN_URL = settings.LOGIN_URL

def tearDown(self):
super(LoginURLSettings, self).tearDown()
settings.LOGIN_URL = self.old_LOGIN_URL

def get_login_required_url(self, login_url):
settings.LOGIN_URL = login_url
response = self.client.get('/login_required/') response = self.client.get('/login_required/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
return response.url self.assertURLEqual(response.url, url)


@override_settings(LOGIN_URL='/login/')
def test_standard_login_url(self): def test_standard_login_url(self):
login_url = '/login/' self.assertLoginURLEquals('/login/?next=/login_required/')
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('', mutable=True) @override_settings(LOGIN_URL='login')
querystring['next'] = '/login_required/' def test_named_login_url(self):
self.assertEqual(login_required_url, 'http://testserver%s?%s' % self.assertLoginURLEquals('/login/?next=/login_required/')
(login_url, querystring.urlencode('/')))


@override_settings(LOGIN_URL='http://remote.example.com/login')
def test_remote_login_url(self): def test_remote_login_url(self):
login_url = 'http://remote.example.com/login' quoted_next = urlquote('http://testserver/login_required/')
login_required_url = self.get_login_required_url(login_url) expected = 'http://remote.example.com/login?next=%s' % quoted_next
querystring = QueryDict('', mutable=True) self.assertLoginURLEquals(expected)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url,
'%s?%s' % (login_url, querystring.urlencode('/')))


@override_settings(LOGIN_URL='https:///login/')
def test_https_login_url(self): def test_https_login_url(self):
login_url = 'https:///login/' quoted_next = urlquote('http://testserver/login_required/')
login_required_url = self.get_login_required_url(login_url) expected = 'https:///login/?next=%s' % quoted_next
querystring = QueryDict('', mutable=True) self.assertLoginURLEquals(expected)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url,
'%s?%s' % (login_url, querystring.urlencode('/')))


@override_settings(LOGIN_URL='/login/?pretty=1')
def test_login_url_with_querystring(self): def test_login_url_with_querystring(self):
login_url = '/login/?pretty=1' self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/')
login_required_url = self.get_login_required_url(login_url)
querystring = QueryDict('pretty=1', mutable=True)
querystring['next'] = '/login_required/'
self.assertEqual(login_required_url, 'http://testserver/login/?%s' %
querystring.urlencode('/'))


@override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
def test_remote_login_url_with_next_querystring(self): def test_remote_login_url_with_next_querystring(self):
login_url = 'http://remote.example.com/login/' quoted_next = urlquote('http://testserver/login_required/')
login_required_url = self.get_login_required_url('%s?next=/default/' % expected = 'http://remote.example.com/login/?next=%s' % quoted_next
login_url) self.assertLoginURLEquals(expected)
querystring = QueryDict('', mutable=True)
querystring['next'] = 'http://testserver/login_required/'
self.assertEqual(login_required_url, '%s?%s' % (login_url, @skipIfCustomUser
querystring.urlencode('/'))) class LoginRedirectUrlTest(AuthViewsTestCase):
"""Tests for settings.LOGIN_REDIRECT_URL."""
def assertLoginRedirectURLEqual(self, url):
response = self.login()
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, url)

def test_default(self):
self.assertLoginRedirectURLEqual('/accounts/profile/')

@override_settings(LOGIN_REDIRECT_URL='/custom/')
def test_custom(self):
self.assertLoginRedirectURLEqual('/custom/')

@override_settings(LOGIN_REDIRECT_URL='password_reset')
def test_named(self):
self.assertLoginRedirectURLEqual('/password_reset/')

@override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/')
def test_remote(self):
self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/')




@skipIfCustomUser @skipIfCustomUser
Expand All @@ -457,11 +543,11 @@ def test_logout_with_overridden_redirect_url(self):
self.login() self.login()
response = self.client.get('/logout/next_page/') response = self.client.get('/logout/next_page/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/somewhere/')) self.assertURLEqual(response.url, '/somewhere/')


response = self.client.get('/logout/next_page/?next=/login/') response = self.client.get('/logout/next_page/?next=/login/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/login/')) self.assertURLEqual(response.url, '/login/')


self.confirm_logged_out() self.confirm_logged_out()


Expand All @@ -470,23 +556,31 @@ def test_logout_with_next_page_specified(self):
self.login() self.login()
response = self.client.get('/logout/next_page/') response = self.client.get('/logout/next_page/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/somewhere/')) self.assertURLEqual(response.url, '/somewhere/')
self.confirm_logged_out() self.confirm_logged_out()


def test_logout_with_redirect_argument(self): def test_logout_with_redirect_argument(self):
"Logout with query string redirects to specified resource" "Logout with query string redirects to specified resource"
self.login() self.login()
response = self.client.get('/logout/?next=/login/') response = self.client.get('/logout/?next=/login/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/login/')) self.assertURLEqual(response.url, '/login/')
self.confirm_logged_out() self.confirm_logged_out()


def test_logout_with_custom_redirect_argument(self): def test_logout_with_custom_redirect_argument(self):
"Logout with custom query string redirects to specified resource" "Logout with custom query string redirects to specified resource"
self.login() self.login()
response = self.client.get('/logout/custom_query/?follow=/somewhere/') response = self.client.get('/logout/custom_query/?follow=/somewhere/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith('/somewhere/')) self.assertURLEqual(response.url, '/somewhere/')
self.confirm_logged_out()

def test_logout_with_named_redirect(self):
"Logout resolves names or URLs passed as next_page."
self.login()
response = self.client.get('/logout/next_page/named/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')
self.confirm_logged_out() self.confirm_logged_out()


def test_security_check(self, password='password'): def test_security_check(self, password='password'):
Expand Down
11 changes: 11 additions & 0 deletions django/contrib/auth/tests/urls.py
Expand Up @@ -62,8 +62,19 @@ def custom_request_auth_login(request):
urlpatterns = urlpatterns + patterns('', urlpatterns = urlpatterns + patterns('',
(r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')), (r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')),
(r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
(r'^logout/next_page/named/$', 'django.contrib.auth.views.logout', dict(next_page='password_reset')),
(r'^remote_user/$', remote_user_auth_view), (r'^remote_user/$', remote_user_auth_view),
(r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
(r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')),
(r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')),
(r'^reset/custom/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'django.contrib.auth.views.password_reset_confirm',
dict(post_reset_redirect='/custom/')),
(r'^reset/custom/named/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'django.contrib.auth.views.password_reset_confirm',
dict(post_reset_redirect='password_reset')),
(r'^password_change/custom/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='/custom/')),
(r'^password_change/custom/named/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='password_reset')),
(r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)),
(r'^login_required/$', login_required(password_reset)), (r'^login_required/$', login_required(password_reset)),
(r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
Expand Down
9 changes: 9 additions & 0 deletions django/contrib/auth/views.py
Expand Up @@ -72,6 +72,9 @@ def logout(request, next_page=None,
""" """
auth_logout(request) auth_logout(request)


if next_page is not None:
next_page = resolve_url(next_page)

if redirect_field_name in request.REQUEST: if redirect_field_name in request.REQUEST:
next_page = request.REQUEST[redirect_field_name] next_page = request.REQUEST[redirect_field_name]
# Security check -- don't allow redirection to a different host. # Security check -- don't allow redirection to a different host.
Expand Down Expand Up @@ -140,6 +143,8 @@ def password_reset(request, is_admin_site=False,
extra_context=None): extra_context=None):
if post_reset_redirect is None: if post_reset_redirect is None:
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done') post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done')
else:
post_reset_redirect = resolve_url(post_reset_redirect)
if request.method == "POST": if request.method == "POST":
form = password_reset_form(request.POST) form = password_reset_form(request.POST)
if form.is_valid(): if form.is_valid():
Expand Down Expand Up @@ -193,6 +198,8 @@ def password_reset_confirm(request, uidb36=None, token=None,
assert uidb36 is not None and token is not None # checked by URLconf assert uidb36 is not None and token is not None # checked by URLconf
if post_reset_redirect is None: if post_reset_redirect is None:
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
else:
post_reset_redirect = resolve_url(post_reset_redirect)
try: try:
uid_int = base36_to_int(uidb36) uid_int = base36_to_int(uidb36)
user = UserModel._default_manager.get(pk=uid_int) user = UserModel._default_manager.get(pk=uid_int)
Expand Down Expand Up @@ -243,6 +250,8 @@ def password_change(request,
current_app=None, extra_context=None): current_app=None, extra_context=None):
if post_change_redirect is None: if post_change_redirect is None:
post_change_redirect = reverse('django.contrib.auth.views.password_change_done') post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
else:
post_change_redirect = resolve_url(post_change_redirect)
if request.method == "POST": if request.method == "POST":
form = password_change_form(user=request.user, data=request.POST) form = password_change_form(user=request.user, data=request.POST)
if form.is_valid(): if form.is_valid():
Expand Down
6 changes: 6 additions & 0 deletions docs/releases/1.6.txt
Expand Up @@ -243,6 +243,12 @@ Minor features


* The reason phrase can be customized in HTTP responses. * The reason phrase can be customized in HTTP responses.


* When giving the URL of the next page for :func:`~django.contrib.auth.views.logout`,
:func:`~django.contrib.auth.views.password_reset`,
:func:`~django.contrib.auth.views.password_reset_confirm`,
and :func:`~django.contrib.auth.views.password_change`, you can now pass
URL names and they will be resolved.

Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================


Expand Down

0 comments on commit 980ae2a

Please sign in to comment.