From cd1eefc46720d3bb16db3bcc05f4a09b05db4fd9 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 28 Oct 2025 20:27:50 +0100 Subject: [PATCH] Fix Django 5.x compatibility and add comprehensive test - Replace deprecated User.make_random_password() with get_random_string() - Add import for django.utils.crypto.get_random_string - Add test_password_reset_w_confirmed_mail_no_password() to verify fix - Test covers specific scenario: user with confirmed email but no password - Ensures password reset works for users with unusable passwords (starting with '!') - All existing password reset tests continue to pass The make_random_password() method was removed entirely in Django 5.x, requiring migration to get_random_string() for generating random passwords. Fixes #102 --- ivatar/ivataraccount/test_views.py | 73 ++++++++++++++++++++++++++++++ ivatar/ivataraccount/views.py | 3 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index ca79849..f21e09a 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1864,6 +1864,79 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods "why are we sending mails to the wrong mail address?", ) + def test_password_reset_w_confirmed_mail_no_password(self): + """ + Test password reset for user with confirmed email but no password set. + This tests the specific case that was failing with Django 4.2+ where + User.objects.make_random_password() was deprecated. + + Reproduces the scenario where a user has a confirmed email address + but their password field is empty or starts with "!" (unusable password). + """ + # Avoid sending out mails + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + + # Create a user with no usable password (starts with "!") + test_user = User.objects.create_user( + username="testuser_no_pass", + email="", # No email in User model + ) + # Set an unusable password (starts with "!") + test_user.set_unusable_password() + test_user.save() + + # Add a confirmed email (this is the key scenario) + confirmed_email = test_user.confirmedemail_set.create(email="test@example.com") + + # Verify the user has no usable password + self.assertTrue( + test_user.password.startswith("!"), + "Test user should have unusable password starting with '!'", + ) + + url = reverse("password_reset") + + # Attempt password reset - this should work without AttributeError + response = self.client.post( + url, + { + "email": confirmed_email.email, + }, + follow=True, + ) + + # Refresh user from database to see changes + test_user.refresh_from_db() + + # Verify the request succeeded + self.assertEqual(response.status_code, 200, "password reset page not working?") + + # Verify that the user now has a usable password (no longer starts with "!") + self.assertFalse( + test_user.password.startswith("!"), + "User should now have a usable password after reset", + ) + + # Verify the email was set on the user object + self.assertEqual( + test_user.email, + confirmed_email.email, + "The password reset view should have set the email on user object", + ) + + # Verify a reset email was sent + self.assertEqual( + len(mail.outbox), 1, "user exists, there should be a mail in the outbox!" + ) + self.assertEqual( + mail.outbox[0].to[0], + test_user.email, + "reset email should be sent to the correct address", + ) + + # Clean up + test_user.delete() + def test_export(self): """ Test if export works diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index c754583..76df478 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -29,6 +29,7 @@ from django.contrib.auth.views import LoginView from django.contrib.auth.views import ( PasswordResetView as PasswordResetViewOriginal, ) +from django.utils.crypto import get_random_string from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse_lazy, reverse @@ -1252,7 +1253,7 @@ class PasswordResetView(PasswordResetViewOriginal): # reset request if user: if not user.password or user.password.startswith("!"): - random_pass = User.make_random_password() + random_pass = get_random_string(12) user.set_password(random_pass) user.save()