リダイレクト処理をテストするときに気を付けること

## 初めに

Laravelで認証処理を開発していたのですが、リダイレクト処理に関するテストを作る際に躓いたので、解決案や注意点を備忘録としてまとめておきます。

## 環境

* `PHP v7.4`
* `Laravel v6.20`
* `PHPUnit v9.5`
* `Laravel/ui v1.3` (`Laravel/ui`が提供する認証機能を使っています)

## 前提となるシチュエーション

* 「ログイン済みユーザーが再度ログインAPIへアクセスした場合、ユーザー情報をJSONで返す処理」をテストします。

* ログイン認証自体は`Laravel/ui`によるものを使うので、ログインユーザーが再度ログインしようとすると、`RedirectIfAuthenticated.php`で決めたurl、今回では`/api/user`にリダイレクトします。

* `/api/user`というエンドポイントは以下のような`Auth::user()`の結果を返すだけの処理です。

```php:api.php
Route::get("/user", fn() => Auth::user())->name("user");
```

`name("user")`と名付けたので、以降このエンドポイントへは`route("user")`でアクセスできるということを頭の片隅に置いておいてください。


## 起きた問題

「ログイン済みユーザーが再度ログインAPIへアクセスした場合、ユーザー情報をJSONで返す処理」

を1つのテストファイルで検証しようとすると、レスポンスにユーザー情報となるJSONが含まれていませんでした。

レスポンスをのぞいてみると、JSONが含まれない代わりに

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="refresh" content="0;url='http://localhost/api/user'" />
  <title>Redirecting to http://localhost/api/user</title>
</head>
<body>
  Redirecting to <a href="http://localhost/api/user">http://localhost/api/user</a>.
</body>
</html>
```

のようなHTMLがレスポンスに含まれていました。

## 原因

テストファイルで受け取ったレスポンスというのは、「アクセス直後に`/api/user`にリダイレクトするよ」という内容のHTMLです。

先ほどのHTML内

```html
<meta http-equiv="refresh" content="0;url='http://localhost/api/user'" />
```

という`<meta>`タグが、HTMLリダイレクトを行うタグになります。

クライアントはこの`<meta>`タグの書かれたHTMLを受け取ると、指定した時間の後（今回は0なので即座に）そのurlへリクエストを送ります。

つまりリダイレクト処理では

「リダイレクト前urlへのリクエスト」と「リダイレクト後のJSONレスポンス」

というやり取りの間に、

**「自動でリダイレクト先へリクエストを送るHTMLレスポンス」**と
**「リダイレクトurlへのリクエスト」**

が暗黙的に行われています。

この処理を経てユーザー情報がJSONで返却されます。

リダイレクトについてのの詳細は[HTTP のリダイレクト - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Redirections)をご覧ください。

## 結論

以上の、リクエストとレスポンスのペアが実は2つあるという点を踏まえて

「ログイン済みユーザーが再度ログインAPIへアクセスした場合、ユーザー情報をJSONで返す処理」

をテストしたい場合、**リダイレクト前**と**リダイレクト後**の2つに分けて考え、それぞれについて検証するテストを作成すればいいです。

今回の例では、具体的に次のことを確かめればいいです。

1. ログインAPIへアクセスしたらちゃんと期待したエンドポイントへリダイレクトされるか（リダイレクト前の検証）
2. リダイレクト先での挙動が期待したものになっているか（リダイレクト後の検証）


### 1. のテスト内容

1.のことをテストするためには

* ステータスコード300番台が返ってくる
* 期待したurlへリダイレクトする

という2点をテストすれば大丈夫です。

例えば、認証済みの時は指定したurlへリダイレクトするミドルウェアである`RedirectIfAuthenticated`クラスを、以下のように変更したとします。

```php:RedirectIfAuthenticated.php

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
    public function handle($request, Closure $next, $guard = null)
    {
        // 現在ログイン済みであれば分岐
        if (Auth::guard($guard)->check()) {
            // web.phpもしくはapi.phpでname("user")としたエンドポイントへリダイレクト
            return redirect()->route('user');  // ★ここを変更
        }

        return $next($request);
    }
}
```

この処理を検証するテストコードの例は以下の通りです。

```php:LoginApiTest.php

use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class LoginApiTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        // テスト環境のDBにユーザーを作成
        $this->user = factory(User::class)->create(); 
        // ログイン時の入力内容を作成
        $this->data = ['email' => $this->user->email, 'password' => 'password']; 
    }
     /**
     * @test
     */
    public function should_ログインユーザーが再度ログイン処理にアクセスしたらuserエンドポイントへリダイレクトされる(): void
    {
        // $this->userとしてログインした状態で再度ログインしようとする
        $response = $this->actingAs($this->user)->postJson(route('login'), $this->data);

        // ステータスコード302番が返ってきて、期待したurlへリダイレクトされるかテスト
        $response->assertStatus(302)->assertRedirect(route("user"));
    }
}

```

### 2. のテスト内容

今回の例で、2. での「リダイレクト先での挙動」に当てはまるものは

「ユーザー情報をJSONの形式で返す」

です。

なのでそのことを確かめるテストを作ります。

テストコードの例は以下の通りです。

```php:GetUserApiTest.php

use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class GetUserApiTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        // テスト環境のDBにユーザーを作成
        $this->user = factory(User::class)->create();
    }

    /**
     * @test
     */
    public function should_ユーザーがログイン中であればユーザーデータを返す(): void
    {
        // $this->userとしてログインした状態で`route("user")`APIにアクセス
        $response = $this->actingAs($this->user)->getJson(route('user'));

        // JSONレスポンスにこのユーザー情報が含まれることを期待する
        $expected = [
            'name' => $this->user['name'],
            'email' => $this->user['email'],
        ];
        
        // ステータスコードがが200番で、JSONレスポンスに$expetedの内容が含まれるかテスト
        $response->assertStatus(200)->assertJson($expected);
    }
}
```

## 終わりに

テストコード内で受け取ったレスポンスは

「自動でリダイレクト先へリクエストを送るHTMLレスポンス」

であったことが、躓きの直接の原因でした。

ですのでリダイレクト処理に関するテストコードを書く際は、リダイレクト前とリダイレクト後の2つに分けて考えられるという点を踏まえて、本文中に示した2つの事柄を検証すれば大丈夫です。

同様のことで悩んでいた方はこの記事を読むことで解決の助けになればと思います。