Laravelユーザー認証のテスト4回目、今回はパスワードリセットについてです。ユーザーがパスワードを忘れた時、「パスワードを忘れた?」などのリンクからパスワードリセットリクエストを送信する、よく実装されている機能です。

この記事では、Laravel10xにBreezeをインストールした環境でテストを作成しています。テスト環境構築を含む以前の記事は、以下のリンクからご覧いただけます。

テスト対象画面とテスト項目

パスワードリセットの機能をテストするにあたり、テスト対象はパスワードリセットのリクエスト画面パスワードリセット画面の2つがあります。それぞれの画面のテスト項目にはさまざまなケースが考えられますが、今回は以下としました。

パスワードリセットのリクエスト画面

  • パスワードリセットリクエスト画面にアクセスできる
  • メールアドレスを入力し、登録済みのメールアドレスならユーザー宛てにリセットメールが送信される
  • 未登録のメールアドレスであればメールは送信されない

パスワードリセット画面

  • メールに記載されたリセット用のリンクからパスワードリセット画面へアクセスできる
  • パスワードを新しいものに変更、その後新しいパスワードでログインができる

パスワードリセットリクエスト画面のテスト

ではまずは、「パスワードリセットリクエスト画面にアクセスできる」のテストからです。

    public function test_reset_password_link_screen_can_be_rendered(): void
    {
        $response = $this->get('/forgot-password');

        $response->assertStatus(200);
    }

ここは特に変わった記述はありません。未ログイン状態でアクセスすることが前提なので、テストコードは2行で完成です。

次は「メールアドレスを入力し、登録済みのメールアドレスならユーザー宛てにリセットメールが送信される」のテストですが、その前にポスト送信時に実行されるコードを確認してみると、以下のようになっていました。


namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;

class PasswordResetLinkController extends Controller
{
...
    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'email' => ['required', 'email'],
        ]);

        // We will send the password reset link to this user. Once we have attempted
        // to send the link, we will examine the response then see the message we
        // need to show to the user. Finally, we'll send out a proper response.
        $status = Password::sendResetLink(
            $request->only('email')
        );

        return $status == Password::RESET_LINK_SENT
                    ? back()->with('status', __($status))
                    : back()->withInput($request->only('email'))
                            ->withErrors(['email' => __($status)]);
    }
}

こちらを踏まえて、Breezeが提供してくれているテストにリダイレクト先・セッションメッセージのアサートを追加しました。以下がそのテストコードになります。

    public function test_reset_password_link_can_be_requested(): void
    {
        Notification::fake(); //メールが実際に送信されないように

        $user = User::factory()->create();

        $response = $this->from('forgot-password')
            ->post('/forgot-password', ['email' => $user->email]);

        $response->assertRedirect('forgot-password')
            ->assertSessionHas('status', 'パスワードリセットメールを送信しました。');

        Notification::assertSentTo($user, ResetPassword::class);
    }

ステータスメッセージとともに前画面にリダイレクトされ、ユーザー宛てにメールが正常に送信されることを確認しています。

$this->from('forgot-password')ですが、これは何をしているかというとリファラーをセットしています。
テストでは、ブラウザ操作時のように「前のURL」という概念が存在しません。そのためfrom()関数を使ってポスト送信時のリファラをセットしてあげる必要があります。

3つ目は、「未登録のメールアドレスであればメールは送信されない」のテストです。このケースはBreezeのテストコードには含まれていなかったため新規に作成しています。

    public function test_reset_password_link_request_fails_for_unregistered_email(): void
    {
        Notification::fake();

        $response = $this->from('forgot-password')
            ->post('/forgot-password', ['email' => 'unregistered@example.com']);

        $response->assertRedirect('forgot-password')
            ->assertSessionHasErrors('email', ['このメールアドレスに一致するユーザーは存在しません。']);

        Notification::assertNothingSent();
    }

from()を使ってリファラをセット・ポスト送信をするところまでは先ほどと同じです。もちろんメールアドレスは未登録のものを使用してくださいね。ポスト送信の結果からassertSessionHasErrors()を使用して、エラーメッセージemailが期待通りであることをアサートしています。

最後のassertNothingSent()は、通知が何も送信されていないことをアサートしています。

ではここまでで作成したテストを実行してみましょう。

$ php artisan test --filter=PasswordResetTest

   PASS  Tests\Feature\Auth\PasswordResetTest
  ✓ reset password link screen can be rendered                                                                    0.57s  
  ✓ reset password link can be requested                                                                          0.16s  
  ✓ reset password link request fails for unregistered email                                                      0.17s  

  Tests:    3 passed (10 assertions)
  Duration: 1.14s

問題なさそうです。

パスワードリセット画面でのテスト

次は、パスワードリセット実行画面のテストです。まずは「メールに記載されたリセット用のリンクからパスワードリセット画面へアクセスできる」をテストします。

    public function test_reset_password_screen_can_be_rendered(): void
    {
        Notification::fake();

        $user = User::factory()->create();

        $this->post('/forgot-password', ['email' => $user->email]);

        Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
            $response = $this->get('/reset-password/' . $notification->token);

            $response->assertStatus(200);

            return true;
        });
    }

上記はBreezeのテストコードから特に変更はしていません。コードの前半部分はユーザーの作成とポスト送信に関する記述なので先ほどまでのテストと同じで、後半のNotification::assertSentToの部分がこのテストのメインとなります。

クロージャーの引数として、$notificationを受け取っています。これはユーザーへ送信される通知メールであるResetPasswordクラスのインスタンスなので、これを使って「メールが送信されていること」だけでなく「通知メールに含まれるトークンを使ってパスワードリセット画面(/reset-password/{token})にアクセスし、そのページが正常にレンダリングされる」ことの一連のテストが可能になります。

では次が最後のテスト項目です。「パスワードを新しいものに変更、その後新しいパスワードでログインができる」をテストします。Breezeのテストコードをベースに、成功時のセッションメッセージと新しいパスワードでログインできることのアサートを追加しました。

    public function test_password_can_be_reset_with_valid_token(): void
    {
        Notification::fake();

        $user = User::factory()->create();

        $this->post('/forgot-password', ['email' => $user->email]);

        Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
            $response = $this->post('/reset-password', [
                'token' => $notification->token,
                'email' => $user->email,
                'password' => 'new_password', //新しいパスワード
                'password_confirmation' => 'new_password',
            ]);

            $response->assertRedirect('login')
                ->assertSessionHas('status', 'パスワードをリセットしました。');

            return true;
        });

        $this->assertGuest(); //この時点ではまだ未ログイン状態であることを確認

        $this->post('/login', [
            'email' => $user->email,
            'password' => 'new_password', //新しいパスワードでログイン
        ]);

        $this->assertAuthenticated(); //ログインできたことを確認
    }

このテストでもNotification::assertSentToが使用されていますが、今度はクロージャーの中でパスワードリセットのポスト送信を実行し、リダイレクト先・ステータスメッセージが期待通りであることを確認しています。

また、パスワードリセット完了直後はユーザーはまだ未ログイン状態となっています。そのため新しいパスワード(new_password)を使ってログインを実行し、ログインが成功することを確認しています。

ではここで作成した2つのテストを実行してみます。

$ php artisan test --filter=PasswordResetTest

   PASS  Tests\Feature\Auth\PasswordResetTest
  ✓ reset password screen can be rendered                                                                         0.60s  
  ✓ password can be reset with valid token                                                                        0.10s  

  Tests:    2 passed (8 assertions)
  Duration: 0.89s

うまくいきました!次回は、ログインスロットルに関するテストをご紹介します。

By hmatsu