I'm trying to implement an integration test for the password reset flow, but I'm stuck at the "password_reset_confirm" view. I already tested the flow manually, and it works fine. Unfortunately, the Django unit test client seems unable to follow correctly the redirects required in this view.
urls config
from django.contrib.auth import views as auth_views
url(r"^accounts/password_change/$",
auth_views.PasswordChangeView.as_view(),
name="password_change"),
url(r"^accounts/password_change/done/$",
auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done"),
url(r"^accounts/password_reset/$",
auth_views.PasswordResetView.as_view(email_template_name="app/email/accounts/password_reset_email.html",
success_url=reverse_lazy("app:password_reset_done"),
subject_template_name="app/email/accounts/password_reset_subject.html"),
name="password_reset"),
url(r"^accounts/password_reset/done/$",
auth_views.PasswordResetDoneView.as_view(),
name="password_reset_done"),
url(r"^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
auth_views.PasswordResetConfirmView.as_view(
success_url=reverse_lazy("app:password_reset_complete"),
form_class=CustomSetPasswordForm),
name="password_reset_confirm"),
url(r"^accounts/reset/complete/$",
auth_views.PasswordResetCompleteView.as_view(),
name="password_reset_complete"),
Test code
import re
from django.urls import reverse, NoReverseMatch
from django.test import TestCase, Client
from django.core import mail
from django.test.utils import override_settings
from django.contrib.auth import authenticate
VALID_USER_NAME = "username"
USER_OLD_PSW = "oldpassword"
USER_NEW_PSW = "newpassword"
PASSWORD_RESET_URL = reverse("app:password_reset")
def PASSWORD_RESET_CONFIRM_URL(uidb64, token):
try:
return reverse("app:password_reset_confirm", args=(uidb64, token))
except NoReverseMatch:
return f"/accounts/reset/invaliduidb64/invalid-token/"
def utils_extract_reset_tokens(full_url):
return re.findall(r"/([\w\-]+)",
re.search(r"^http\://.+$", full_url, flags=re.MULTILINE)[0])[3:5]
@override_settings(EMAIL_BACKEND="anymail.backends.test.EmailBackend")
class PasswordResetTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.myclient = Client()
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, token),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
Now, the assert fails: the user is authenticated with the old password. From the log I'm able to detect that the change password is not executed.
A few extra useful information:
post
returns a successfulHTTP 200
;- the
response.redirect_chain
is[('/accounts/reset/token_removed/set-password/', 302)]
and I think this is wrong, as it should have another loop (in the manual case I see another call to the dispatch method); - I'm executing the test with the Django unit test tools.
Any idea on how to properly test this scenario? I need this to make sure emails and logging are properly executed (and never removed).
Many thanks!
EDIT: solution
As well explained by the accepted solution, here the working code for the test case:
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
self.myclient.get(PASSWORD_RESET_CONFIRM_URL(uidb64, token), follow=True)
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, "set-password"),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))