Password validation with SimilarityValidator and CustomUser Django

I am trying to validate a password against CustomUser fields: email and full_name.

All test validations are working except for UserAttributeSimilarityValidator, which is the only test that I have included with the code below.

forms.py

    class RegistrationForm(forms.ModelForm):
        email = forms.EmailField(label=_('Email address'),
                                 widget=forms.EmailInput(attrs={'class': 'form-control mb-4',
                                                            'class': 'form-control mb-4',
                                                            }))

        full_name = forms.CharField(label=_('Full name'),
                                    widget=forms.TextInput(attrs={'class': 'form-control mb-4',
                                                              }))
        password = forms.CharField(label=_('Password'),
                                   widget=forms.PasswordInput(attrs={'class': 'form-control mb-4',
                                                                 'autocomplete': 'new-password',
                                                                 'id': 'psw',
                                                                 }))

        class Meta:
            model = get_user_model()
            fields = ('full_name', 'email', 'password')

        def clean_password(self):
            password = self.cleaned_data.get("password")
            if password:
                try:
                    password_validation.validate_password(password, self.instance)
                except ValidationError as error:
                    self.add_error("password", error)
            return password

        def clean_email(self):
            email = self.cleaned_data['email']
            if User.objects.filter(email=email).exists():
                raise forms.ValidationError(
                mark_safe(_(f'A user with that email already exists, click this <br><a href="{reverse("account:pwdreset")}">Password Reset</a> link'
                      ' to recover your account.'))
            )

            return email

test.py

@override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {
                'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
                'OPTIONS': {
                    'user_attributes': (
                            'email', 'full_name',
                    )},
            },

        ]
    )
    def test_validates_password(self):
        data = {
            "email": "jsmith@example.com",
            "full_name": "John Smith",
            "password": "jsmith",
        }
        form = RegistrationForm(data)
        self.assertFalse(form.is_valid()
        self.assertEqual(len(form["password"].errors), 1)

        self.assertIn(
            "The password is too similar to the email.",
            form["password"].errors,
        )

I am trying to validate a password against CustomUser fields: email and full_name.

All test validations are working except for UserAttributeSimilarityValidator, which is the only test that I have included with the code below.

forms.py

    class RegistrationForm(forms.ModelForm):
        email = forms.EmailField(label=_('Email address'),
                                 widget=forms.EmailInput(attrs={'class': 'form-control mb-4',
                                                            'class': 'form-control mb-4',
                                                            }))

        full_name = forms.CharField(label=_('Full name'),
                                    widget=forms.TextInput(attrs={'class': 'form-control mb-4',
                                                              }))
        password = forms.CharField(label=_('Password'),
                                   widget=forms.PasswordInput(attrs={'class': 'form-control mb-4',
                                                                 'autocomplete': 'new-password',
                                                                 'id': 'psw',
                                                                 }))

        class Meta:
            model = get_user_model()
            fields = ('full_name', 'email', 'password')

        def clean_password(self):
            password = self.cleaned_data.get("password")
            if password:
                try:
                    password_validation.validate_password(password, self.instance)
                except ValidationError as error:
                    self.add_error("password", error)
            return password

        def clean_email(self):
            email = self.cleaned_data['email']
            if User.objects.filter(email=email).exists():
                raise forms.ValidationError(
                mark_safe(_(f'A user with that email already exists, click this <br><a href="{reverse("account:pwdreset")}">Password Reset</a> link'
                      ' to recover your account.'))
            )

            return email

tests.py

@override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {
                'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
                'OPTIONS': {
                    'user_attributes': (
                            'email', 'full_name',
                    )},
            },

        ]
    )
    def test_validates_password(self):
        data = {
            "email": "jsmith@example.com",
            "full_name": "John Smith",
            "password": "jsmith",
        }
        form = RegistrationForm(data)
        self.assertFalse(form.is_valid()
        self.assertEqual(len(form["password"].errors), 1)

        self.assertIn(
            "The password is too similar to the email.",
            form["password"].errors,
        )

Which results in this test result:

FAIL: test_validates_password (account.tests.test_forms.RegistrationFormTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tester/Documents/dev/test/venv/lib/python3.8/site-packages/django/test/utils.py", line 437, in inner
    return func(*args, **kwargs)
  File "/Users/tester/Documents/dev/test/account/tests/test_forms.py", line 112, in test_validates_password
    self.assertFalse(form.is_valid())
AssertionError: True is not false

----------------------------------------------------------------------
Ran 5 tests in 0.868s

FAILED (failures=1)
Destroying test database for alias 'default'...

Which is completely logical because the UserAttributeSimilarityValidator is not picking up the similarity between the email and the password fields.

Please, what am I doing wrong?

My first guess - and that’s all this really is, just a guess - is that the validator is trying to compare the field submitted in the form to the user object to which that ModelForm applies. If you’re not passing a user object to the form as the instance, there’s nothing for the password to compare to - the validator is not matching a field in the form to different fields in the form.
(I could be greatly misinterpreting what I’m reading, but this is my gut hunch.)

See the docs at Password management in Django | Django documentation | Django

Thanks Ken, yes, of course.

UserAttributeSimilarityValidator explains it all…

 def validate(self, password, user=None):
        if not user:
            return

I got around this by copying from django.contrib.auth.forms.UserCreationForm and added/edited my form with:

    def clean_password(self):
        password = self.cleaned_data.get("password")
        return password

    def _post_clean(self):
        super()._post_clean()
        # Validate the password after self.instance is updated with form data
        # by super().
        password = self.cleaned_data.get("password")
        if password:
            try:
                password_validation.validate_password(password, self.instance)
            except ValidationError as error:
                self.add_error("password", error)

    def save(self, commit=True):
        """
        Save user.
        Save the provided password in hashed format.
        :return custom_user.models.EmailUser: user
        """
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password"])
        if commit:
            user.save()
        return user

Thanks again Ken and have a great weekend!