import datetime import re import sys import urllib.parse from unittest import mock from django import forms from django.contrib.auth.forms import ( AdminPasswordChangeForm, AdminUserCreationForm, AuthenticationForm, BaseUserCreationForm, PasswordChangeForm, PasswordResetForm, ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, SetPasswordForm, SetPasswordMixin, 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.fields import CharField, Field, IntegerField from django.test import RequestFactory, 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 django.views.debug import technical_500_response from django.views.decorators.debug import sensitive_variables 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), "") 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, "", ) 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]) def test_custom_form_with_non_required_password(self): class CustomUserCreationForm(BaseUserCreationForm): password1 = forms.CharField(required=False) password2 = forms.CharField(required=False) another_field = forms.CharField(required=True) data = { "username": "testclientnew", "another_field": "Content", } form = CustomUserCreationForm(data) self.assertIs(form.is_valid(), True, form.errors) 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'([^<]*)' 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'^Link$', 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"}), '
' "

" " algorithm: pbkdf2_sha256" " iterations: 100000" " salt: a6Pucb******" " hash: " " WmCkn9**************************************" "

" '

Reset password

' "
", ) 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