Laravelユーザー認証のテスト、5回目となる今回はスロットル機能についてです。ログイン画面でEメール・パスワードを入力し一定回数以上ログインに失敗するとロックがかかる便利機能ですが、ユニットテストはどのように書くのでしょうか。

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

テスト対象


デフォルトではパスワードを5回連続で間違えると60秒ロックがかかり、ロック中はこの画像のような状態となり正しいパスワードを入力してもログインができません。

実際には、このロック機能はEメール+IPをキーとして働いており、同じIPからでもメールアドレスを変えればログイン試行が可能です。こういったことを踏まえると様々なテストケースが考えられますが、今回はシンプルに以下の項目をテストします。

・正しいEメールとパスワードでログインできる
・Eメールと誤ったパスワードを入力、5回失敗までは通常のエラーメッセージが返る
・60秒経過するまでは正しいパスワードを入力してもログイン不可
・60秒経過後はログインできる

ログインスロットルのテスト

まずは正しいEメールとパスワードで問題なくログインできることのテストです。このテストはBreezeが提供してくれているテストコードに含まれており、以前にログイン機能のテストでもご紹介しています。

    public function test_users_can_authenticate_using_the_login_screen(): void
    {
        $user = User::factory()->create();

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

        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }

ここは特に問題ないと思いますのでスロットル機能のテストに進みます。スロットルに関してはBreezeのテストコードが無かったので新規に作成しました。少し長いですが、以下が一連のテストコードになります。

    public function test_login_throttle(): void
    {
        $user = User::factory()->create(
            ['password' => 'password']
        );

        //5回の失敗までは通常のエラーメッセージが返る
        for ($i = 0; $i < 5; ++$i) {

            $response = $this->post('/login', [
                'email' => $user->email,
                'password' => 'wrong-password', //異なるパスワード
            ]);

            $response->assertSessionHasErrors(['email' => 'ログイン情報が存在しません。']);
        };

        //5回失敗後、正しいパスワードでログイン試行
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        $response->assertSessionHasErrors([
            'email' => 'ログイン試行の規定数に達しました。59秒後に再度お試しください。'
        ]);

        //50秒経過
        sleep(50);

        //まだ60秒経過していないので正しいパスワードでもログイン不可
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        //メッセージの部分比較
        $response->assertInvalid([
            'email' => 'ログイン試行の規定数に達しました。'
        ]);

        sleep(10);

        //60秒経過後はログインできる
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }

まずは、正しいEメールと間違ったパスワードで5回ログインを試行しています。当然ログインは失敗しますが、その際のメッセージは通常のログイン失敗のエラーメッセージであることを確認しています。

次に、6回目に正しいEメールとパスワードでログインします。ですがロックがかかっているためログインはできません。その際のエラーメッセージは「ログイン試行の規定数に達しました。・・・」であることを確認しています。

つづいて50秒後にも、正しいEメールとパスワードで再度ログインを試行して、ログイン不可の確認をします。

上のコードでは「50秒後」の時間設定にはsleep(50)を使用していますが、sleep()を使うと実際にその秒数待ち時間が入るため、待ち時間が長い場合は以下のようにCarbonを使うことで実際に待つことなくテストが実行できます。

...
use Carbon\Carbon;
...
        // 50秒経過
        Carbon::setTestNow(Carbon::now()->addSeconds(50));
        // sleep(50);
...

また、「ログイン試行の規定数に達しました。**秒後に再度・・・」のエラーメッセージのアサートですが、秒数部分はどうしてもテストごとに差異が出てしまいます。エラーメッセージのアサートとして馴染みのあるassertSessionHasErrors()は全文比較するコマンドのため、メッセージの一部のみを確認したい場合はLaravel8から追加されたassertInvalid()が便利です。

使い方はassertSessionHasErrors()と同じですが、以下のようにメッセージの部分比較が可能です。

       $response->assertSessionHasErrors([
            'email' => 'ログイン試行の規定数に達しました。59秒後に再度お試しください。'
        ]);

        $response->assertInvalid([
            'email' => 'ログイン試行の規定数に達しました。'
        ]);

最後に、さらに10秒待って、今度は60秒経過したのでログイン可能なことを確認です。ここでも、sleep()の代わりに先ほどのCarbonのメソッドを使用して効率的にテストを進めることが可能です。

では、テストを実行してみます。

  $ php artisan test --filter=AuthenticationTest

   PASS  Tests\Feature\Auth\AuthenticationTest
  ✓ users can authenticate using the login screen                                                                                                        0.28s  
  ✓ login throttle                                                                                                                                      61.16s  

  Tests:    2 passed (20 assertions)

問題なくテストが成功しました。画面でのテストでは少し手間がかかる機能もこうやってユニットテストで確認できるのは便利ですね。

By hmatsu