mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
8c35a0a903
On successful submission of a password reset request, an email is sent to the accounts known to the system. If sending this email fails (due to email backend misconfiguration, service provider outage, network issues, etc.), an attacker might exploit this by detecting which password reset requests succeed and which ones generate a 500 error response. Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak, Adam Johnson, and Sarah Boyce for the reviews.
1674 lines
62 KiB
Python
1674 lines
62 KiB
Python
import datetime
|
||
import re
|
||
import urllib.parse
|
||
from unittest import mock
|
||
|
||
from django.contrib.auth.forms import (
|
||
AdminPasswordChangeForm,
|
||
AdminUserCreationForm,
|
||
AuthenticationForm,
|
||
BaseUserCreationForm,
|
||
PasswordChangeForm,
|
||
PasswordResetForm,
|
||
ReadOnlyPasswordHashField,
|
||
ReadOnlyPasswordHashWidget,
|
||
SetPasswordForm,
|
||
UserChangeForm,
|
||
UserCreationForm,
|
||
UsernameField,
|
||
)
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.contrib.auth.models import User
|
||
from django.contrib.auth.signals import user_login_failed
|
||
from django.contrib.sites.models import Site
|
||
from django.core import mail
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.mail import EmailMultiAlternatives
|
||
from django.forms import forms
|
||
from django.forms.fields import CharField, Field, IntegerField
|
||
from django.test import SimpleTestCase, TestCase, override_settings
|
||
from django.urls import reverse
|
||
from django.utils import translation
|
||
from django.utils.text import capfirst
|
||
from django.utils.translation import gettext as _
|
||
|
||
from .models.custom_user import (
|
||
CustomUser,
|
||
CustomUserWithoutIsActiveField,
|
||
ExtensionUser,
|
||
)
|
||
from .models.with_custom_email_field import CustomEmailField
|
||
from .models.with_integer_username import IntegerUsernameUser
|
||
from .models.with_many_to_many import CustomUserWithM2M, Organization
|
||
from .settings import AUTH_TEMPLATES
|
||
|
||
|
||
class TestDataMixin:
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
cls.u1 = User.objects.create_user(
|
||
username="testclient", password="password", email="testclient@example.com"
|
||
)
|
||
cls.u2 = User.objects.create_user(
|
||
username="inactive", password="password", is_active=False
|
||
)
|
||
cls.u3 = User.objects.create_user(username="staff", password="password")
|
||
cls.u4 = User.objects.create(username="empty_password", password="")
|
||
cls.u5 = User.objects.create(username="unmanageable_password", password="$")
|
||
cls.u6 = User.objects.create(username="unknown_password", password="foo$bar")
|
||
cls.u7 = User.objects.create(
|
||
username="unusable_password", password=make_password(None)
|
||
)
|
||
|
||
|
||
class ExtraValidationFormMixin:
|
||
def __init__(self, *args, failing_fields=None, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.failing_fields = failing_fields or {}
|
||
|
||
def failing_helper(self, field_name):
|
||
if field_name in self.failing_fields:
|
||
errors = [
|
||
ValidationError(error, code="invalid")
|
||
for error in self.failing_fields[field_name]
|
||
]
|
||
raise ValidationError(errors)
|
||
return self.cleaned_data[field_name]
|
||
|
||
|
||
class BaseUserCreationFormTest(TestDataMixin, TestCase):
|
||
|
||
form_class = BaseUserCreationForm
|
||
|
||
def test_form_fields(self):
|
||
form = self.form_class()
|
||
self.assertEqual(
|
||
list(form.fields.keys()), ["username", "password1", "password2"]
|
||
)
|
||
|
||
def test_user_already_exists(self):
|
||
data = {
|
||
"username": "testclient",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["username"].errors,
|
||
[str(User._meta.get_field("username").error_messages["unique"])],
|
||
)
|
||
|
||
def test_invalid_data(self):
|
||
data = {
|
||
"username": "jsmith!",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
validator = next(
|
||
v
|
||
for v in User._meta.get_field("username").validators
|
||
if v.code == "invalid"
|
||
)
|
||
self.assertEqual(form["username"].errors, [str(validator.message)])
|
||
|
||
def test_password_verification(self):
|
||
# The verification password is incorrect.
|
||
data = {
|
||
"username": "jsmith",
|
||
"password1": "test123",
|
||
"password2": "test",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["password2"].errors, [str(form.error_messages["password_mismatch"])]
|
||
)
|
||
|
||
def test_both_passwords(self):
|
||
# One (or both) passwords weren't given
|
||
data = {"username": "jsmith"}
|
||
form = self.form_class(data)
|
||
required_error = [str(Field.default_error_messages["required"])]
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(form["password1"].errors, required_error)
|
||
self.assertEqual(form["password2"].errors, required_error)
|
||
|
||
data["password2"] = "test123"
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(form["password1"].errors, required_error)
|
||
self.assertEqual(form["password2"].errors, [])
|
||
|
||
@mock.patch("django.contrib.auth.password_validation.password_changed")
|
||
def test_success(self, password_changed):
|
||
# The success case.
|
||
data = {
|
||
"username": "jsmith@example.com",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save(commit=False)
|
||
self.assertEqual(password_changed.call_count, 0)
|
||
u = form.save()
|
||
self.assertEqual(password_changed.call_count, 1)
|
||
self.assertEqual(repr(u), "<User: jsmith@example.com>")
|
||
|
||
def test_unicode_username(self):
|
||
data = {
|
||
"username": "宝",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertTrue(form.is_valid())
|
||
u = form.save()
|
||
self.assertEqual(u.username, "宝")
|
||
|
||
def test_normalize_username(self):
|
||
# The normalization happens in AbstractBaseUser.clean() and ModelForm
|
||
# validation calls Model.clean().
|
||
ohm_username = "testΩ" # U+2126 OHM SIGN
|
||
data = {
|
||
"username": ohm_username,
|
||
"password1": "pwd2",
|
||
"password2": "pwd2",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertTrue(form.is_valid())
|
||
user = form.save()
|
||
self.assertNotEqual(user.username, ohm_username)
|
||
self.assertEqual(user.username, "testΩ") # U+03A9 GREEK CAPITAL LETTER OMEGA
|
||
|
||
def test_invalid_username_no_normalize(self):
|
||
field = UsernameField(max_length=254)
|
||
# Usernames are not normalized if they are too long.
|
||
self.assertEqual(field.to_python("½" * 255), "½" * 255)
|
||
self.assertEqual(field.to_python("ff" * 254), "ff" * 254)
|
||
|
||
def test_duplicate_normalized_unicode(self):
|
||
"""
|
||
To prevent almost identical usernames, visually identical but differing
|
||
by their unicode code points only, Unicode NFKC normalization should
|
||
make appear them equal to Django.
|
||
"""
|
||
omega_username = "iamtheΩ" # U+03A9 GREEK CAPITAL LETTER OMEGA
|
||
ohm_username = "iamtheΩ" # U+2126 OHM SIGN
|
||
self.assertNotEqual(omega_username, ohm_username)
|
||
User.objects.create_user(username=omega_username, password="pwd")
|
||
data = {
|
||
"username": ohm_username,
|
||
"password1": "pwd2",
|
||
"password2": "pwd2",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.errors["username"], ["A user with that username already exists."]
|
||
)
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||
),
|
||
"OPTIONS": {
|
||
"min_length": 12,
|
||
},
|
||
},
|
||
]
|
||
)
|
||
def test_validates_password(self):
|
||
data = {
|
||
"username": "otherclient",
|
||
"password1": "otherclient",
|
||
"password2": "otherclient",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(len(form["password2"].errors), 2)
|
||
self.assertIn(
|
||
"The password is too similar to the username.", form["password2"].errors
|
||
)
|
||
self.assertIn(
|
||
"This password is too short. It must contain at least 12 characters.",
|
||
form["password2"].errors,
|
||
)
|
||
|
||
def test_password_whitespace_not_stripped(self):
|
||
data = {
|
||
"username": "testuser",
|
||
"password1": " testpassword ",
|
||
"password2": " testpassword ",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.cleaned_data["password1"], data["password1"])
|
||
self.assertEqual(form.cleaned_data["password2"], data["password2"])
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
]
|
||
)
|
||
def test_password_help_text(self):
|
||
form = self.form_class()
|
||
self.assertEqual(
|
||
form.fields["password1"].help_text,
|
||
"<ul><li>"
|
||
"Your password can’t be too similar to your other personal information."
|
||
"</li></ul>",
|
||
)
|
||
|
||
def test_password_extra_validations(self):
|
||
class ExtraValidationForm(ExtraValidationFormMixin, self.form_class):
|
||
def clean_password1(self):
|
||
return self.failing_helper("password1")
|
||
|
||
def clean_password2(self):
|
||
return self.failing_helper("password2")
|
||
|
||
data = {"username": "extra", "password1": "abc", "password2": "abc"}
|
||
for fields in (["password1"], ["password2"], ["password1", "password2"]):
|
||
with self.subTest(fields=fields):
|
||
errors = {field: [f"Extra validation for {field}."] for field in fields}
|
||
form = ExtraValidationForm(data, failing_fields=errors)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertDictEqual(form.errors, errors)
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
]
|
||
)
|
||
def test_user_create_form_validates_password_with_all_data(self):
|
||
"""
|
||
BaseUserCreationForm password validation uses all of the form's data.
|
||
"""
|
||
|
||
class CustomUserCreationForm(self.form_class):
|
||
class Meta(self.form_class.Meta):
|
||
model = User
|
||
fields = ("username", "email", "first_name", "last_name")
|
||
|
||
form = CustomUserCreationForm(
|
||
{
|
||
"username": "testuser",
|
||
"password1": "testpassword",
|
||
"password2": "testpassword",
|
||
"first_name": "testpassword",
|
||
"last_name": "lastname",
|
||
}
|
||
)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.errors["password2"],
|
||
["The password is too similar to the first name."],
|
||
)
|
||
|
||
def test_username_field_autocapitalize_none(self):
|
||
form = self.form_class()
|
||
self.assertEqual(
|
||
form.fields["username"].widget.attrs.get("autocapitalize"), "none"
|
||
)
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
form = self.form_class()
|
||
tests = (
|
||
("username", "username"),
|
||
("password1", "new-password"),
|
||
("password2", "new-password"),
|
||
)
|
||
for field_name, autocomplete in tests:
|
||
with self.subTest(field_name=field_name, autocomplete=autocomplete):
|
||
self.assertEqual(
|
||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||
)
|
||
|
||
|
||
class CustomUserCreationFormTest(TestDataMixin, TestCase):
|
||
|
||
def test_custom_form(self):
|
||
class CustomUserCreationForm(BaseUserCreationForm):
|
||
class Meta(BaseUserCreationForm.Meta):
|
||
model = ExtensionUser
|
||
fields = UserCreationForm.Meta.fields + ("date_of_birth",)
|
||
|
||
data = {
|
||
"username": "testclient",
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
"date_of_birth": "1988-02-24",
|
||
}
|
||
form = CustomUserCreationForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
|
||
def test_custom_form_with_different_username_field(self):
|
||
class CustomUserCreationForm(BaseUserCreationForm):
|
||
class Meta(BaseUserCreationForm.Meta):
|
||
model = CustomUser
|
||
fields = ("email", "date_of_birth")
|
||
|
||
data = {
|
||
"email": "test@client222.com",
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
"date_of_birth": "1988-02-24",
|
||
}
|
||
form = CustomUserCreationForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
|
||
def test_custom_form_hidden_username_field(self):
|
||
class CustomUserCreationForm(BaseUserCreationForm):
|
||
class Meta(BaseUserCreationForm.Meta):
|
||
model = CustomUserWithoutIsActiveField
|
||
fields = ("email",) # without USERNAME_FIELD
|
||
|
||
data = {
|
||
"email": "testclient@example.com",
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
}
|
||
form = CustomUserCreationForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
|
||
def test_custom_form_saves_many_to_many_field(self):
|
||
class CustomUserCreationForm(BaseUserCreationForm):
|
||
class Meta(BaseUserCreationForm.Meta):
|
||
model = CustomUserWithM2M
|
||
fields = UserCreationForm.Meta.fields + ("orgs",)
|
||
|
||
organization = Organization.objects.create(name="organization 1")
|
||
|
||
data = {
|
||
"username": "testclient@example.com",
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
"orgs": [str(organization.pk)],
|
||
}
|
||
form = CustomUserCreationForm(data)
|
||
self.assertIs(form.is_valid(), True)
|
||
user = form.save(commit=True)
|
||
self.assertSequenceEqual(user.orgs.all(), [organization])
|
||
|
||
|
||
class UserCreationFormTest(BaseUserCreationFormTest):
|
||
|
||
form_class = UserCreationForm
|
||
|
||
def test_case_insensitive_username(self):
|
||
data = {
|
||
"username": "TeStClIeNt",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = UserCreationForm(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["username"].errors,
|
||
["A user with that username already exists."],
|
||
)
|
||
|
||
@override_settings(AUTH_USER_MODEL="auth_tests.ExtensionUser")
|
||
def test_case_insensitive_username_custom_user_and_error_message(self):
|
||
class CustomUserCreationForm(UserCreationForm):
|
||
class Meta(UserCreationForm.Meta):
|
||
model = ExtensionUser
|
||
fields = UserCreationForm.Meta.fields + ("date_of_birth",)
|
||
error_messages = {
|
||
"username": {"unique": "This username has already been taken."}
|
||
}
|
||
|
||
ExtensionUser.objects.create_user(
|
||
username="testclient",
|
||
password="password",
|
||
email="testclient@example.com",
|
||
date_of_birth=datetime.date(1984, 3, 5),
|
||
)
|
||
data = {
|
||
"username": "TeStClIeNt",
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
"date_of_birth": "1980-01-01",
|
||
}
|
||
form = CustomUserCreationForm(data)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertEqual(
|
||
form["username"].errors,
|
||
["This username has already been taken."],
|
||
)
|
||
|
||
|
||
# To verify that the login form rejects inactive users, use an authentication
|
||
# backend that allows them.
|
||
@override_settings(
|
||
AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.AllowAllUsersModelBackend"]
|
||
)
|
||
class AuthenticationFormTest(TestDataMixin, TestCase):
|
||
def test_invalid_username(self):
|
||
# The user submits an invalid username.
|
||
|
||
data = {
|
||
"username": "jsmith_does_not_exist",
|
||
"password": "test123",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.non_field_errors(),
|
||
[
|
||
form.error_messages["invalid_login"]
|
||
% {"username": User._meta.get_field("username").verbose_name}
|
||
],
|
||
)
|
||
|
||
def test_inactive_user(self):
|
||
# The user is inactive.
|
||
data = {
|
||
"username": "inactive",
|
||
"password": "password",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.non_field_errors(), [str(form.error_messages["inactive"])]
|
||
)
|
||
|
||
# Use an authentication backend that rejects inactive users.
|
||
@override_settings(
|
||
AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.ModelBackend"]
|
||
)
|
||
def test_inactive_user_incorrect_password(self):
|
||
"""An invalid login doesn't leak the inactive status of a user."""
|
||
data = {
|
||
"username": "inactive",
|
||
"password": "incorrect",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.non_field_errors(),
|
||
[
|
||
form.error_messages["invalid_login"]
|
||
% {"username": User._meta.get_field("username").verbose_name}
|
||
],
|
||
)
|
||
|
||
def test_login_failed(self):
|
||
signal_calls = []
|
||
|
||
def signal_handler(**kwargs):
|
||
signal_calls.append(kwargs)
|
||
|
||
user_login_failed.connect(signal_handler)
|
||
fake_request = object()
|
||
try:
|
||
form = AuthenticationForm(
|
||
fake_request,
|
||
{
|
||
"username": "testclient",
|
||
"password": "incorrect",
|
||
},
|
||
)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertIs(signal_calls[0]["request"], fake_request)
|
||
finally:
|
||
user_login_failed.disconnect(signal_handler)
|
||
|
||
def test_inactive_user_i18n(self):
|
||
with (
|
||
self.settings(USE_I18N=True),
|
||
translation.override("pt-br", deactivate=True),
|
||
):
|
||
# The user is inactive.
|
||
data = {
|
||
"username": "inactive",
|
||
"password": "password",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form.non_field_errors(), [str(form.error_messages["inactive"])]
|
||
)
|
||
|
||
# Use an authentication backend that allows inactive users.
|
||
@override_settings(
|
||
AUTHENTICATION_BACKENDS=[
|
||
"django.contrib.auth.backends.AllowAllUsersModelBackend"
|
||
]
|
||
)
|
||
def test_custom_login_allowed_policy(self):
|
||
# The user is inactive, but our custom form policy allows them to log in.
|
||
data = {
|
||
"username": "inactive",
|
||
"password": "password",
|
||
}
|
||
|
||
class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm):
|
||
def confirm_login_allowed(self, user):
|
||
pass
|
||
|
||
form = AuthenticationFormWithInactiveUsersOkay(None, data)
|
||
self.assertTrue(form.is_valid())
|
||
|
||
# Raise a ValidationError in the form to disallow some logins according
|
||
# to custom logic.
|
||
class PickyAuthenticationForm(AuthenticationForm):
|
||
def confirm_login_allowed(self, user):
|
||
if user.username == "inactive":
|
||
raise ValidationError("This user is disallowed.")
|
||
raise ValidationError("Sorry, nobody's allowed in.")
|
||
|
||
form = PickyAuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(form.non_field_errors(), ["This user is disallowed."])
|
||
|
||
data = {
|
||
"username": "testclient",
|
||
"password": "password",
|
||
}
|
||
form = PickyAuthenticationForm(None, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."])
|
||
|
||
def test_success(self):
|
||
# The success case
|
||
data = {
|
||
"username": "testclient",
|
||
"password": "password",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.non_field_errors(), [])
|
||
|
||
def test_unicode_username(self):
|
||
User.objects.create_user(username="Σαρα", password="pwd")
|
||
data = {
|
||
"username": "Σαρα",
|
||
"password": "pwd",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.non_field_errors(), [])
|
||
|
||
@override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
|
||
def test_username_field_max_length_matches_user_model(self):
|
||
self.assertEqual(CustomEmailField._meta.get_field("username").max_length, 255)
|
||
data = {
|
||
"username": "u" * 255,
|
||
"password": "pwd",
|
||
"email": "test@example.com",
|
||
}
|
||
CustomEmailField.objects.create_user(**data)
|
||
form = AuthenticationForm(None, data)
|
||
self.assertEqual(form.fields["username"].max_length, 255)
|
||
self.assertEqual(form.fields["username"].widget.attrs.get("maxlength"), 255)
|
||
self.assertEqual(form.errors, {})
|
||
|
||
@override_settings(AUTH_USER_MODEL="auth_tests.IntegerUsernameUser")
|
||
def test_username_field_max_length_defaults_to_254(self):
|
||
self.assertIsNone(IntegerUsernameUser._meta.get_field("username").max_length)
|
||
data = {
|
||
"username": "0123456",
|
||
"password": "password",
|
||
}
|
||
IntegerUsernameUser.objects.create_user(**data)
|
||
form = AuthenticationForm(None, data)
|
||
self.assertEqual(form.fields["username"].max_length, 254)
|
||
self.assertEqual(form.fields["username"].widget.attrs.get("maxlength"), 254)
|
||
self.assertEqual(form.errors, {})
|
||
|
||
def test_username_field_label(self):
|
||
class CustomAuthenticationForm(AuthenticationForm):
|
||
username = CharField(label="Name", max_length=75)
|
||
|
||
form = CustomAuthenticationForm()
|
||
self.assertEqual(form["username"].label, "Name")
|
||
|
||
def test_username_field_label_not_set(self):
|
||
class CustomAuthenticationForm(AuthenticationForm):
|
||
username = CharField()
|
||
|
||
form = CustomAuthenticationForm()
|
||
username_field = User._meta.get_field(User.USERNAME_FIELD)
|
||
self.assertEqual(
|
||
form.fields["username"].label, capfirst(username_field.verbose_name)
|
||
)
|
||
|
||
def test_username_field_autocapitalize_none(self):
|
||
form = AuthenticationForm()
|
||
self.assertEqual(
|
||
form.fields["username"].widget.attrs.get("autocapitalize"), "none"
|
||
)
|
||
|
||
def test_username_field_label_empty_string(self):
|
||
class CustomAuthenticationForm(AuthenticationForm):
|
||
username = CharField(label="")
|
||
|
||
form = CustomAuthenticationForm()
|
||
self.assertEqual(form.fields["username"].label, "")
|
||
|
||
def test_password_whitespace_not_stripped(self):
|
||
data = {
|
||
"username": "testuser",
|
||
"password": " pass ",
|
||
}
|
||
form = AuthenticationForm(None, data)
|
||
form.is_valid() # Not necessary to have valid credentails for the test.
|
||
self.assertEqual(form.cleaned_data["password"], data["password"])
|
||
|
||
@override_settings(AUTH_USER_MODEL="auth_tests.IntegerUsernameUser")
|
||
def test_integer_username(self):
|
||
class CustomAuthenticationForm(AuthenticationForm):
|
||
username = IntegerField()
|
||
|
||
user = IntegerUsernameUser.objects.create_user(username=0, password="pwd")
|
||
data = {
|
||
"username": 0,
|
||
"password": "pwd",
|
||
}
|
||
form = CustomAuthenticationForm(None, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.cleaned_data["username"], data["username"])
|
||
self.assertEqual(form.cleaned_data["password"], data["password"])
|
||
self.assertEqual(form.errors, {})
|
||
self.assertEqual(form.user_cache, user)
|
||
|
||
def test_get_invalid_login_error(self):
|
||
error = AuthenticationForm().get_invalid_login_error()
|
||
self.assertIsInstance(error, ValidationError)
|
||
self.assertEqual(
|
||
error.message,
|
||
"Please enter a correct %(username)s and password. Note that both "
|
||
"fields may be case-sensitive.",
|
||
)
|
||
self.assertEqual(error.code, "invalid_login")
|
||
self.assertEqual(error.params, {"username": "username"})
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
form = AuthenticationForm()
|
||
tests = (
|
||
("username", "username"),
|
||
("password", "current-password"),
|
||
)
|
||
for field_name, autocomplete in tests:
|
||
with self.subTest(field_name=field_name, autocomplete=autocomplete):
|
||
self.assertEqual(
|
||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||
)
|
||
|
||
def test_no_password(self):
|
||
data = {"username": "username"}
|
||
form = AuthenticationForm(None, data)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertEqual(
|
||
form["password"].errors, [Field.default_error_messages["required"]]
|
||
)
|
||
|
||
|
||
class SetPasswordFormTest(TestDataMixin, TestCase):
|
||
def test_password_verification(self):
|
||
# The two new passwords do not match.
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"new_password1": "abc123",
|
||
"new_password2": "abc",
|
||
}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["new_password2"].errors,
|
||
[str(form.error_messages["password_mismatch"])],
|
||
)
|
||
|
||
@mock.patch("django.contrib.auth.password_validation.password_changed")
|
||
def test_success(self, password_changed):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"new_password1": "abc123",
|
||
"new_password2": "abc123",
|
||
}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save(commit=False)
|
||
self.assertEqual(password_changed.call_count, 0)
|
||
form.save()
|
||
self.assertEqual(password_changed.call_count, 1)
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||
),
|
||
"OPTIONS": {
|
||
"min_length": 12,
|
||
},
|
||
},
|
||
]
|
||
)
|
||
def test_validates_password(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"new_password1": "testclient",
|
||
"new_password2": "testclient",
|
||
}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(len(form["new_password2"].errors), 2)
|
||
self.assertIn(
|
||
"The password is too similar to the username.", form["new_password2"].errors
|
||
)
|
||
self.assertIn(
|
||
"This password is too short. It must contain at least 12 characters.",
|
||
form["new_password2"].errors,
|
||
)
|
||
|
||
# SetPasswordForm does not consider usable_password for form validation
|
||
data = {
|
||
"new_password1": "testclient",
|
||
"new_password2": "testclient",
|
||
"usable_password": "false",
|
||
}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(len(form["new_password2"].errors), 2)
|
||
self.assertIn(
|
||
"The password is too similar to the username.", form["new_password2"].errors
|
||
)
|
||
self.assertIn(
|
||
"This password is too short. It must contain at least 12 characters.",
|
||
form["new_password2"].errors,
|
||
)
|
||
|
||
def test_no_password(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {"new_password1": "new-password"}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertEqual(
|
||
form["new_password2"].errors, [Field.default_error_messages["required"]]
|
||
)
|
||
form = SetPasswordForm(user, {})
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertEqual(
|
||
form["new_password1"].errors, [Field.default_error_messages["required"]]
|
||
)
|
||
self.assertEqual(
|
||
form["new_password2"].errors, [Field.default_error_messages["required"]]
|
||
)
|
||
|
||
def test_password_whitespace_not_stripped(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"new_password1": " password ",
|
||
"new_password2": " password ",
|
||
}
|
||
form = SetPasswordForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.cleaned_data["new_password1"], data["new_password1"])
|
||
self.assertEqual(form.cleaned_data["new_password2"], data["new_password2"])
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||
),
|
||
"OPTIONS": {
|
||
"min_length": 12,
|
||
},
|
||
},
|
||
]
|
||
)
|
||
def test_help_text_translation(self):
|
||
french_help_texts = [
|
||
"Votre mot de passe ne peut pas trop ressembler à vos autres informations "
|
||
"personnelles.",
|
||
"Votre mot de passe doit contenir au minimum 12 caractères.",
|
||
]
|
||
form = SetPasswordForm(self.u1)
|
||
with translation.override("fr"):
|
||
html = form.as_p()
|
||
for french_text in french_help_texts:
|
||
self.assertIn(french_text, html)
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
form = SetPasswordForm(self.u1)
|
||
tests = (
|
||
("new_password1", "new-password"),
|
||
("new_password2", "new-password"),
|
||
)
|
||
for field_name, autocomplete in tests:
|
||
with self.subTest(field_name=field_name, autocomplete=autocomplete):
|
||
self.assertEqual(
|
||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||
)
|
||
|
||
def test_password_extra_validations(self):
|
||
class ExtraValidationForm(ExtraValidationFormMixin, SetPasswordForm):
|
||
def clean_new_password1(self):
|
||
return self.failing_helper("new_password1")
|
||
|
||
def clean_new_password2(self):
|
||
return self.failing_helper("new_password2")
|
||
|
||
user = User.objects.get(username="testclient")
|
||
data = {"new_password1": "abc", "new_password2": "abc"}
|
||
for fields in (
|
||
["new_password1"],
|
||
["new_password2"],
|
||
["new_password1", "new_password2"],
|
||
):
|
||
with self.subTest(fields=fields):
|
||
errors = {field: [f"Extra validation for {field}."] for field in fields}
|
||
form = ExtraValidationForm(user, data, failing_fields=errors)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertDictEqual(form.errors, errors)
|
||
|
||
|
||
class PasswordChangeFormTest(TestDataMixin, TestCase):
|
||
def test_incorrect_password(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"old_password": "test",
|
||
"new_password1": "abc123",
|
||
"new_password2": "abc123",
|
||
}
|
||
form = PasswordChangeForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["old_password"].errors,
|
||
[str(form.error_messages["password_incorrect"])],
|
||
)
|
||
|
||
def test_password_verification(self):
|
||
# The two new passwords do not match.
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"old_password": "password",
|
||
"new_password1": "abc123",
|
||
"new_password2": "abc",
|
||
}
|
||
form = PasswordChangeForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(
|
||
form["new_password2"].errors,
|
||
[str(form.error_messages["password_mismatch"])],
|
||
)
|
||
|
||
@mock.patch("django.contrib.auth.password_validation.password_changed")
|
||
def test_success(self, password_changed):
|
||
# The success case.
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"old_password": "password",
|
||
"new_password1": "abc123",
|
||
"new_password2": "abc123",
|
||
}
|
||
form = PasswordChangeForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save(commit=False)
|
||
self.assertEqual(password_changed.call_count, 0)
|
||
form.save()
|
||
self.assertEqual(password_changed.call_count, 1)
|
||
|
||
def test_field_order(self):
|
||
# Regression test - check the order of fields:
|
||
user = User.objects.get(username="testclient")
|
||
self.assertEqual(
|
||
list(PasswordChangeForm(user, {}).fields),
|
||
["old_password", "new_password1", "new_password2"],
|
||
)
|
||
|
||
def test_password_whitespace_not_stripped(self):
|
||
user = User.objects.get(username="testclient")
|
||
user.set_password(" oldpassword ")
|
||
data = {
|
||
"old_password": " oldpassword ",
|
||
"new_password1": " pass ",
|
||
"new_password2": " pass ",
|
||
}
|
||
form = PasswordChangeForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.cleaned_data["old_password"], data["old_password"])
|
||
self.assertEqual(form.cleaned_data["new_password1"], data["new_password1"])
|
||
self.assertEqual(form.cleaned_data["new_password2"], data["new_password2"])
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
user = User.objects.get(username="testclient")
|
||
form = PasswordChangeForm(user)
|
||
self.assertEqual(
|
||
form.fields["old_password"].widget.attrs["autocomplete"], "current-password"
|
||
)
|
||
|
||
|
||
class UserChangeFormTest(TestDataMixin, TestCase):
|
||
def test_username_validity(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {"username": "not valid"}
|
||
form = UserChangeForm(data, instance=user)
|
||
self.assertFalse(form.is_valid())
|
||
validator = next(
|
||
v
|
||
for v in User._meta.get_field("username").validators
|
||
if v.code == "invalid"
|
||
)
|
||
self.assertEqual(form["username"].errors, [str(validator.message)])
|
||
|
||
def test_bug_14242(self):
|
||
# A regression test, introduce by adding an optimization for the
|
||
# UserChangeForm.
|
||
|
||
class MyUserForm(UserChangeForm):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields["groups"].help_text = (
|
||
"These groups give users different permissions"
|
||
)
|
||
|
||
class Meta(UserChangeForm.Meta):
|
||
fields = ("groups",)
|
||
|
||
# Just check we can create it
|
||
MyUserForm({})
|
||
|
||
def test_unusable_password(self):
|
||
user = User.objects.get(username="unusable_password")
|
||
form = UserChangeForm(instance=user)
|
||
self.assertIn(_("No password set."), form.as_table())
|
||
|
||
def test_bug_17944_empty_password(self):
|
||
user = User.objects.get(username="empty_password")
|
||
form = UserChangeForm(instance=user)
|
||
self.assertIn(_("No password set."), form.as_table())
|
||
|
||
def test_bug_17944_unmanageable_password(self):
|
||
user = User.objects.get(username="unmanageable_password")
|
||
form = UserChangeForm(instance=user)
|
||
self.assertIn(
|
||
_("Invalid password format or unknown hashing algorithm."), form.as_table()
|
||
)
|
||
|
||
def test_bug_17944_unknown_password_algorithm(self):
|
||
user = User.objects.get(username="unknown_password")
|
||
form = UserChangeForm(instance=user)
|
||
self.assertIn(
|
||
_("Invalid password format or unknown hashing algorithm."), form.as_table()
|
||
)
|
||
|
||
def test_bug_19133(self):
|
||
"The change form does not return the password value"
|
||
# Use the form to construct the POST data
|
||
user = User.objects.get(username="testclient")
|
||
form_for_data = UserChangeForm(instance=user)
|
||
post_data = form_for_data.initial
|
||
|
||
# The password field should be readonly, so anything
|
||
# posted here should be ignored; the form will be
|
||
# valid, and give back the 'initial' value for the
|
||
# password field.
|
||
post_data["password"] = "new password"
|
||
form = UserChangeForm(instance=user, data=post_data)
|
||
|
||
self.assertTrue(form.is_valid())
|
||
# original hashed password contains $
|
||
self.assertIn("$", form.cleaned_data["password"])
|
||
|
||
def test_bug_19349_bound_password_field(self):
|
||
user = User.objects.get(username="testclient")
|
||
form = UserChangeForm(data={}, instance=user)
|
||
# When rendering the bound password field,
|
||
# ReadOnlyPasswordHashWidget needs the initial
|
||
# value to render correctly
|
||
self.assertEqual(form.initial["password"], form["password"].value())
|
||
|
||
@override_settings(ROOT_URLCONF="auth_tests.urls_admin")
|
||
def test_link_to_password_reset_in_user_change_form(self):
|
||
cases = [
|
||
(
|
||
"testclient",
|
||
"Raw passwords are not stored, so there is no way to see "
|
||
"the user’s password.",
|
||
"Reset password",
|
||
),
|
||
(
|
||
"unusable_password",
|
||
"Enable password-based authentication for this user by setting a "
|
||
"password.",
|
||
"Set password",
|
||
),
|
||
]
|
||
password_reset_link = r'<a class="button" href="([^"]*)">([^<]*)</a>'
|
||
for username, expected_help_text, expected_button_label in cases:
|
||
with self.subTest(username=username):
|
||
user = User.objects.get(username=username)
|
||
form = UserChangeForm(data={}, instance=user)
|
||
password_help_text = form.fields["password"].help_text
|
||
self.assertEqual(password_help_text, expected_help_text)
|
||
|
||
matches = re.search(password_reset_link, form.as_p())
|
||
self.assertIsNotNone(matches)
|
||
self.assertEqual(len(matches.groups()), 2)
|
||
url_prefix = f"admin:{user._meta.app_label}_{user._meta.model_name}"
|
||
# URL to UserChangeForm in admin via to_field (instead of pk).
|
||
user_change_url = reverse(f"{url_prefix}_change", args=(user.pk,))
|
||
joined_url = urllib.parse.urljoin(user_change_url, matches.group(1))
|
||
|
||
pw_change_url = reverse(
|
||
f"{url_prefix}_password_change", args=(user.pk,)
|
||
)
|
||
self.assertEqual(joined_url, pw_change_url)
|
||
self.assertEqual(matches.group(2), expected_button_label)
|
||
|
||
def test_custom_form(self):
|
||
class CustomUserChangeForm(UserChangeForm):
|
||
class Meta(UserChangeForm.Meta):
|
||
model = ExtensionUser
|
||
fields = (
|
||
"username",
|
||
"password",
|
||
"date_of_birth",
|
||
)
|
||
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"username": "testclient",
|
||
"password": "testclient",
|
||
"date_of_birth": "1998-02-24",
|
||
}
|
||
form = CustomUserChangeForm(data, instance=user)
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(form.cleaned_data["username"], "testclient")
|
||
self.assertEqual(form.cleaned_data["date_of_birth"], datetime.date(1998, 2, 24))
|
||
|
||
def test_password_excluded(self):
|
||
class UserChangeFormWithoutPassword(UserChangeForm):
|
||
password = None
|
||
|
||
class Meta:
|
||
model = User
|
||
exclude = ["password"]
|
||
|
||
form = UserChangeFormWithoutPassword()
|
||
self.assertNotIn("password", form.fields)
|
||
|
||
def test_username_field_autocapitalize_none(self):
|
||
form = UserChangeForm()
|
||
self.assertEqual(
|
||
form.fields["username"].widget.attrs.get("autocapitalize"), "none"
|
||
)
|
||
|
||
|
||
@override_settings(TEMPLATES=AUTH_TEMPLATES)
|
||
class PasswordResetFormTest(TestDataMixin, TestCase):
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
# This cleanup is necessary because contrib.sites cache
|
||
# makes tests interfere with each other, see #11505
|
||
Site.objects.clear_cache()
|
||
|
||
def assertEmailMessageSent(self, **kwargs):
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
msg = mail.outbox[0]
|
||
for attr, expected in kwargs.items():
|
||
with self.subTest(attr=attr):
|
||
self.assertEqual(getattr(msg, attr), expected)
|
||
return msg
|
||
|
||
def create_dummy_user(self):
|
||
"""
|
||
Create a user and return a tuple (user_object, username, email).
|
||
"""
|
||
username = "jsmith"
|
||
email = "jsmith@example.com"
|
||
user = User.objects.create_user(username, email, "test123")
|
||
return (user, username, email)
|
||
|
||
def test_invalid_email(self):
|
||
data = {"email": "not valid"}
|
||
form = PasswordResetForm(data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(form["email"].errors, [_("Enter a valid email address.")])
|
||
|
||
def test_user_email_unicode_collision(self):
|
||
User.objects.create_user("mike123", "mike@example.org", "test123")
|
||
User.objects.create_user("mike456", "mıke@example.org", "test123")
|
||
data = {"email": "mıke@example.org"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEmailMessageSent(to=["mıke@example.org"])
|
||
|
||
def test_user_email_domain_unicode_collision(self):
|
||
User.objects.create_user("mike123", "mike@ixample.org", "test123")
|
||
User.objects.create_user("mike456", "mike@ıxample.org", "test123")
|
||
data = {"email": "mike@ıxample.org"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEmailMessageSent(to=["mike@ıxample.org"])
|
||
|
||
def test_user_email_unicode_collision_nonexistent(self):
|
||
User.objects.create_user("mike123", "mike@example.org", "test123")
|
||
data = {"email": "mıke@example.org"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def test_user_email_domain_unicode_collision_nonexistent(self):
|
||
User.objects.create_user("mike123", "mike@ixample.org", "test123")
|
||
data = {"email": "mike@ıxample.org"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def test_nonexistent_email(self):
|
||
"""
|
||
Test nonexistent email address. This should not fail because it would
|
||
expose information about registered users.
|
||
"""
|
||
data = {"email": "foo@bar.com"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def test_cleaned_data(self):
|
||
(user, username, email) = self.create_dummy_user()
|
||
data = {"email": email}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save(domain_override="example.com")
|
||
self.assertEqual(form.cleaned_data["email"], email)
|
||
self.assertEmailMessageSent()
|
||
|
||
def test_custom_email_subject(self):
|
||
data = {"email": "testclient@example.com"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
# Since we're not providing a request object, we must provide a
|
||
# domain_override to prevent the save operation from failing in the
|
||
# potential case where contrib.sites is not installed. Refs #16412.
|
||
form.save(domain_override="example.com")
|
||
self.assertEmailMessageSent(subject="Custom password reset on example.com")
|
||
|
||
def test_custom_email_constructor(self):
|
||
data = {"email": "testclient@example.com"}
|
||
|
||
class CustomEmailPasswordResetForm(PasswordResetForm):
|
||
def send_mail(
|
||
self,
|
||
subject_template_name,
|
||
email_template_name,
|
||
context,
|
||
from_email,
|
||
to_email,
|
||
html_email_template_name=None,
|
||
):
|
||
EmailMultiAlternatives(
|
||
"Forgot your password?",
|
||
"Sorry to hear you forgot your password.",
|
||
None,
|
||
[to_email],
|
||
["site_monitor@example.com"],
|
||
headers={"Reply-To": "webmaster@example.com"},
|
||
alternatives=[
|
||
("Really sorry to hear you forgot your password.", "text/html")
|
||
],
|
||
).send()
|
||
|
||
form = CustomEmailPasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
# Since we're not providing a request object, we must provide a
|
||
# domain_override to prevent the save operation from failing in the
|
||
# potential case where contrib.sites is not installed. Refs #16412.
|
||
form.save(domain_override="example.com")
|
||
self.assertEmailMessageSent(
|
||
subject="Forgot your password?",
|
||
bcc=["site_monitor@example.com"],
|
||
content_subtype="plain",
|
||
)
|
||
|
||
def test_preserve_username_case(self):
|
||
"""
|
||
Preserve the case of the user name (before the @ in the email address)
|
||
when creating a user (#5605).
|
||
"""
|
||
user = User.objects.create_user("forms_test2", "tesT@EXAMple.com", "test")
|
||
self.assertEqual(user.email, "tesT@example.com")
|
||
user = User.objects.create_user("forms_test3", "tesT", "test")
|
||
self.assertEqual(user.email, "tesT")
|
||
|
||
def test_inactive_user(self):
|
||
"""
|
||
Inactive user cannot receive password reset email.
|
||
"""
|
||
(user, username, email) = self.create_dummy_user()
|
||
user.is_active = False
|
||
user.save()
|
||
form = PasswordResetForm({"email": email})
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def test_unusable_password(self):
|
||
user = User.objects.create_user("testuser", "test@example.com", "test")
|
||
data = {"email": "test@example.com"}
|
||
form = PasswordResetForm(data)
|
||
self.assertTrue(form.is_valid())
|
||
user.set_unusable_password()
|
||
user.save()
|
||
form = PasswordResetForm(data)
|
||
# The form itself is valid, but no email is sent
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def test_save_plaintext_email(self):
|
||
"""
|
||
Test the PasswordResetForm.save() method with no html_email_template_name
|
||
parameter passed in.
|
||
Test to ensure original behavior is unchanged after the parameter was added.
|
||
"""
|
||
(user, username, email) = self.create_dummy_user()
|
||
form = PasswordResetForm({"email": email})
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
msg = self.assertEmailMessageSent()
|
||
self.assertEqual(len(msg.alternatives), 0)
|
||
message = msg.message()
|
||
self.assertFalse(message.is_multipart())
|
||
self.assertEqual(message.get_content_type(), "text/plain")
|
||
self.assertEqual(message.get("subject"), "Custom password reset on example.com")
|
||
self.assertEqual(message.get_all("to"), [email])
|
||
self.assertTrue(
|
||
re.match(r"^http://example.com/reset/[\w+/-]", message.get_payload())
|
||
)
|
||
|
||
def test_save_html_email_template_name(self):
|
||
"""
|
||
Test the PasswordResetForm.save() method with html_email_template_name
|
||
parameter specified.
|
||
Test to ensure that a multipart email is sent with both text/plain
|
||
and text/html parts.
|
||
"""
|
||
(user, username, email) = self.create_dummy_user()
|
||
form = PasswordResetForm({"email": email})
|
||
self.assertTrue(form.is_valid())
|
||
form.save(
|
||
html_email_template_name="registration/html_password_reset_email.html"
|
||
)
|
||
msg = self.assertEmailMessageSent()
|
||
self.assertEqual(len(msg.alternatives), 1)
|
||
message = msg.message()
|
||
self.assertEqual(message.get("subject"), "Custom password reset on example.com")
|
||
self.assertEqual(len(message.get_payload()), 2)
|
||
self.assertTrue(message.is_multipart())
|
||
self.assertEqual(message.get_payload(0).get_content_type(), "text/plain")
|
||
self.assertEqual(message.get_payload(1).get_content_type(), "text/html")
|
||
self.assertEqual(message.get_all("to"), [email])
|
||
self.assertTrue(
|
||
re.match(
|
||
r"^http://example.com/reset/[\w/-]+",
|
||
message.get_payload(0).get_payload(),
|
||
)
|
||
)
|
||
self.assertTrue(
|
||
re.match(
|
||
r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$',
|
||
message.get_payload(1).get_payload(),
|
||
)
|
||
)
|
||
|
||
@override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
|
||
def test_save_send_email_exceptions_are_catched_and_logged(self):
|
||
(user, username, email) = self.create_dummy_user()
|
||
form = PasswordResetForm({"email": email})
|
||
self.assertTrue(form.is_valid())
|
||
|
||
with self.assertLogs("django.contrib.auth", level=0) as cm:
|
||
form.save()
|
||
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
self.assertEqual(len(cm.output), 1)
|
||
errors = cm.output[0].split("\n")
|
||
pk = user.pk
|
||
self.assertEqual(
|
||
errors[0],
|
||
f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}",
|
||
)
|
||
self.assertEqual(
|
||
errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
|
||
)
|
||
|
||
@override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
|
||
def test_custom_email_field(self):
|
||
email = "test@mail.com"
|
||
CustomEmailField.objects.create_user("test name", "test password", email)
|
||
form = PasswordResetForm({"email": email})
|
||
self.assertTrue(form.is_valid())
|
||
form.save()
|
||
self.assertEqual(form.cleaned_data["email"], email)
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
self.assertEqual(mail.outbox[0].to, [email])
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
form = PasswordResetForm()
|
||
self.assertEqual(form.fields["email"].widget.attrs["autocomplete"], "email")
|
||
|
||
|
||
class ReadOnlyPasswordHashTest(SimpleTestCase):
|
||
def test_bug_19349_render_with_none_value(self):
|
||
# Rendering the widget with value set to None
|
||
# mustn't raise an exception.
|
||
widget = ReadOnlyPasswordHashWidget()
|
||
html = widget.render(name="password", value=None, attrs={})
|
||
self.assertIn(_("No password set."), html)
|
||
|
||
@override_settings(
|
||
PASSWORD_HASHERS=["django.contrib.auth.hashers.PBKDF2PasswordHasher"]
|
||
)
|
||
def test_render(self):
|
||
widget = ReadOnlyPasswordHashWidget()
|
||
value = (
|
||
"pbkdf2_sha256$100000$a6Pucb1qSFcD$WmCkn9Hqidj48NVe5x0FEM6A9YiOqQcl/83m2Z5u"
|
||
"dm0="
|
||
)
|
||
self.assertHTMLEqual(
|
||
widget.render("name", value, {"id": "id_password"}),
|
||
'<div id="id_password">'
|
||
" <p>"
|
||
" <strong>algorithm</strong>: <bdi>pbkdf2_sha256</bdi>"
|
||
" <strong>iterations</strong>: <bdi>100000</bdi>"
|
||
" <strong>salt</strong>: <bdi>a6Pucb******</bdi>"
|
||
" <strong>hash</strong>: "
|
||
" <bdi>WmCkn9**************************************</bdi>"
|
||
" </p>"
|
||
' <p><a class="button" href="../password/">Reset password</a></p>'
|
||
"</div>",
|
||
)
|
||
|
||
def test_readonly_field_has_changed(self):
|
||
field = ReadOnlyPasswordHashField()
|
||
self.assertIs(field.disabled, True)
|
||
self.assertFalse(field.has_changed("aaa", "bbb"))
|
||
|
||
def test_label(self):
|
||
"""
|
||
ReadOnlyPasswordHashWidget doesn't contain a for attribute in the
|
||
<label> because it doesn't have any labelable elements.
|
||
"""
|
||
|
||
class TestForm(forms.Form):
|
||
hash_field = ReadOnlyPasswordHashField()
|
||
|
||
bound_field = TestForm()["hash_field"]
|
||
self.assertIsNone(bound_field.field.widget.id_for_label("id"))
|
||
self.assertEqual(bound_field.label_tag(), "<label>Hash field:</label>")
|
||
|
||
|
||
class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
|
||
@mock.patch("django.contrib.auth.password_validation.password_changed")
|
||
def test_success(self, password_changed):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"password1": "test123",
|
||
"password2": "test123",
|
||
}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
form.save(commit=False)
|
||
self.assertEqual(password_changed.call_count, 0)
|
||
form.save()
|
||
self.assertEqual(password_changed.call_count, 1)
|
||
self.assertEqual(form.changed_data, ["password"])
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||
),
|
||
"OPTIONS": {
|
||
"min_length": 12,
|
||
},
|
||
},
|
||
]
|
||
)
|
||
def test_validates_password(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
self.assertFalse(form.is_valid())
|
||
self.assertEqual(len(form["password2"].errors), 2)
|
||
self.assertIn(
|
||
"The password is too similar to the username.",
|
||
form["password2"].errors,
|
||
)
|
||
self.assertIn(
|
||
"This password is too short. It must contain at least 12 characters.",
|
||
form["password2"].errors,
|
||
)
|
||
|
||
# passwords are not validated if `usable_password` is unset
|
||
data = {
|
||
"password1": "testclient",
|
||
"password2": "testclient",
|
||
"usable_password": "false",
|
||
}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
self.assertIs(form.is_valid(), True, form.errors)
|
||
|
||
def test_password_whitespace_not_stripped(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {
|
||
"password1": " pass ",
|
||
"password2": " pass ",
|
||
}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
self.assertTrue(form.is_valid())
|
||
self.assertEqual(form.cleaned_data["password1"], data["password1"])
|
||
self.assertEqual(form.cleaned_data["password2"], data["password2"])
|
||
self.assertEqual(form.changed_data, ["password"])
|
||
|
||
def test_password_extra_validations(self):
|
||
class ExtraValidationForm(ExtraValidationFormMixin, AdminPasswordChangeForm):
|
||
def clean_password1(self):
|
||
return self.failing_helper("password1")
|
||
|
||
def clean_password2(self):
|
||
return self.failing_helper("password2")
|
||
|
||
user = User.objects.get(username="testclient")
|
||
data = {"username": "extra", "password1": "abc", "password2": "abc"}
|
||
for fields in (["password1"], ["password2"], ["password1", "password2"]):
|
||
with self.subTest(fields=fields):
|
||
errors = {field: [f"Extra validation for {field}."] for field in fields}
|
||
form = ExtraValidationForm(user, data, failing_fields=errors)
|
||
self.assertIs(form.is_valid(), False)
|
||
self.assertDictEqual(form.errors, errors)
|
||
|
||
def test_non_matching_passwords(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {"password1": "password1", "password2": "password2"}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
self.assertEqual(
|
||
form.errors["password2"], [form.error_messages["password_mismatch"]]
|
||
)
|
||
self.assertEqual(form.changed_data, ["password"])
|
||
|
||
def test_missing_passwords(self):
|
||
user = User.objects.get(username="testclient")
|
||
data = {"password1": "", "password2": ""}
|
||
form = AdminPasswordChangeForm(user, data)
|
||
required_error = [Field.default_error_messages["required"]]
|
||
self.assertEqual(form.errors["password1"], required_error)
|
||
self.assertEqual(form.errors["password2"], required_error)
|
||
self.assertEqual(form.changed_data, [])
|
||
|
||
def test_one_password(self):
|
||
user = User.objects.get(username="testclient")
|
||
form1 = AdminPasswordChangeForm(user, {"password1": "", "password2": "test"})
|
||
required_error = [Field.default_error_messages["required"]]
|
||
self.assertEqual(form1.errors["password1"], required_error)
|
||
self.assertNotIn("password2", form1.errors)
|
||
self.assertEqual(form1.changed_data, [])
|
||
form2 = AdminPasswordChangeForm(user, {"password1": "test", "password2": ""})
|
||
self.assertEqual(form2.errors["password2"], required_error)
|
||
self.assertNotIn("password1", form2.errors)
|
||
self.assertEqual(form2.changed_data, [])
|
||
|
||
def test_html_autocomplete_attributes(self):
|
||
user = User.objects.get(username="testclient")
|
||
form = AdminPasswordChangeForm(user)
|
||
tests = (
|
||
("password1", "new-password"),
|
||
("password2", "new-password"),
|
||
)
|
||
for field_name, autocomplete in tests:
|
||
with self.subTest(field_name=field_name, autocomplete=autocomplete):
|
||
self.assertEqual(
|
||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||
)
|
||
|
||
def test_enable_password_authentication(self):
|
||
user = User.objects.get(username="unusable_password")
|
||
form = AdminPasswordChangeForm(
|
||
user,
|
||
{"password1": "complexpassword", "password2": "complexpassword"},
|
||
)
|
||
self.assertNotIn("usable_password", form.fields)
|
||
self.assertIs(form.is_valid(), True)
|
||
user = form.save(commit=True)
|
||
self.assertIs(user.has_usable_password(), True)
|
||
|
||
def test_disable_password_authentication(self):
|
||
user = User.objects.get(username="testclient")
|
||
form = AdminPasswordChangeForm(
|
||
user,
|
||
{"usable_password": "false", "password1": "", "password2": "test"},
|
||
)
|
||
self.assertIn("usable_password", form.fields)
|
||
self.assertIn(
|
||
"If disabled, the current password for this user will be lost.",
|
||
form.fields["usable_password"].help_text,
|
||
)
|
||
self.assertIs(form.is_valid(), True) # Valid despite password empty/mismatch.
|
||
user = form.save(commit=True)
|
||
self.assertIs(user.has_usable_password(), False)
|
||
|
||
|
||
class AdminUserCreationFormTest(BaseUserCreationFormTest):
|
||
|
||
form_class = AdminUserCreationForm
|
||
|
||
def test_form_fields(self):
|
||
form = self.form_class()
|
||
self.assertEqual(
|
||
list(form.fields.keys()),
|
||
["username", "password1", "password2", "usable_password"],
|
||
)
|
||
|
||
@override_settings(
|
||
AUTH_PASSWORD_VALIDATORS=[
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation."
|
||
"UserAttributeSimilarityValidator"
|
||
)
|
||
},
|
||
{
|
||
"NAME": (
|
||
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||
),
|
||
"OPTIONS": {
|
||
"min_length": 12,
|
||
},
|
||
},
|
||
]
|
||
)
|
||
def test_no_password_validation_if_unusable_password_set(self):
|
||
data = {
|
||
"username": "otherclient",
|
||
"password1": "otherclient",
|
||
"password2": "otherclient",
|
||
"usable_password": "false",
|
||
}
|
||
form = self.form_class(data)
|
||
# Passwords are not validated if `usable_password` is unset.
|
||
self.assertIs(form.is_valid(), True, form.errors)
|
||
|
||
class CustomUserCreationForm(self.form_class):
|
||
class Meta(self.form_class.Meta):
|
||
model = User
|
||
fields = ("username", "email", "first_name", "last_name")
|
||
|
||
form = CustomUserCreationForm(
|
||
{
|
||
"username": "testuser",
|
||
"password1": "testpassword",
|
||
"password2": "testpassword",
|
||
"first_name": "testpassword",
|
||
"last_name": "lastname",
|
||
"usable_password": "false",
|
||
}
|
||
)
|
||
self.assertIs(form.is_valid(), True, form.errors)
|
||
|
||
def test_unusable_password(self):
|
||
data = {
|
||
"username": "new-user-which-does-not-exist",
|
||
"usable_password": "false",
|
||
}
|
||
form = self.form_class(data)
|
||
self.assertIs(form.is_valid(), True, form.errors)
|
||
u = form.save()
|
||
self.assertEqual(u.username, data["username"])
|
||
self.assertFalse(u.has_usable_password())
|