8

パスワード リセット フローの統合テストを実装しようとしていますが、「password_reset_confirm」ビューでスタックしています。すでにフローを手動でテストしましたが、正常に動作します。残念ながら、Django 単体テスト クライアントは、このビューで必要なリダイレクトに正しく従うことができないようです。

URL 構成

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

テストコード

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))

ここで、アサートは失敗します。ユーザーは古いパスワードで認証されます。ログから、パスワードの変更が実行されていないことを検出できます。

いくつかの追加の有用な情報:

  • post成功を返しますHTTP 200
  • これは別のループが必要なので、これは間違ってresponse.redirect_chainいる[('/accounts/reset/token_removed/set-password/', 302)]と思います (手動の場合、ディスパッチ メソッドへの別の呼び出しが表示されます)。
  • Django 単体テスト ツールを使用してテストを実行しています。

このシナリオを適切にテストする方法について何か考えはありますか? これは、電子メールとログが適切に実行される (削除されない) ことを確認するために必要です。

どうもありがとう!

編集:解決策

受け入れられたソリューションでよく説明されているように、テストケースの作業コードは次のとおりです。

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))
4

1 に答える 1