Livewireのmountのauthorizeエラーをアサートする

投稿日 最終更新日

2024年10月29日

Livewireのmount()メソッド内のauthroizeエラーをLaravelテストでアサートする方法を説明します。

mount()メソッド内のauthorize

例えば以下のような感じのLivewireのmount()内のauhtorize。

    public function mount($postId)
    {
        $post = Post::find($postId);
        // このauthorizeエラーをアサートしたい
        $this->authorize("update", post);

        ...
    }

通常の方法だとアサートできない

これ、普通にLivewire3のドキュメントにある通りに、

<?php

namespace Tests\Feature\Livewire;

use App\Livewire\UpdatePost;
use Livewire\Livewire;
use App\Models\User;
use App\Models\Post;
use Tests\TestCase;

class UpdatePostTest extends TestCase
{
    /** @test */
    public function cant_update_another_users_post()
    {
        $user = User::factory()->create();
        $stranger = User::factory()->create();

        $post = Post::factory()->for($stranger)->create();

        Livewire::actingAs($user)
            ->test(UpdatePost::class, ['post' => $post])
            ->set('title', 'Living the lavender life')
            ->call('save')
            ->assertUnauthorized();
    }
}

のようにやってもアサートできず、

ErrorException
  Trying to access array offset on value of type null

  at vendor/livewire/livewire/src/Mechanisms/HandleComponents/HandleComponents.php:88
     84▕     }
     85▕
     86▕     public function update($snapshot, $updates, $calls)
     87▕     {
  88▕         $data = $snapshot['data'];
     89▕         $memo = $snapshot['memo'];
     90▕
     91▕         if (config('app.debug')) $start = microtime(true);
     92▕         [ $component, $context ] = $this->fromSnapshot($snapshot);

のようなエラーになる。

何が起きているんだろう

結論から言えば、私の力ではわからない。
でも一応色々やって考察したものを残す。

恐らく、以下のような流れでエラーになっている。

  1. mount()でAuthorizationExceptionが発生
  2. mount()でエラーが発生することでLivewireがしっかり初期化されない
  3. Laravelの例外ハンドラがエラーを受け取りHTTPレスポンスに変換する
  4. Livewireのsnapshotが作成されないままHTTPレスポンスが作成される
  5. snapshotが無いので表示する過程でエラー

という感じだと思う。
そもそもLaravelが例外ハンドラというもので勝手にHTTPレスポンスに変換してくれていたことさえ知らなかったよ私は。

じゃあどうすればいいか

公式ドキュメントにあったassertUnauthorized()

    /**
     * Assert that the response has a 401 "Unauthorized" status code.
     *
     * @return $this
     */
    public function assertUnauthorized()
    {
        return $this->assertStatus(401);
    }

のように401レスポンスを期待するもの。

恐らく本来はLaravelの例外ハンドラがAuthorizationExceptionを401のレスポンスに変換してくれる。
しかし、今回はレスポンス変換に失敗してしまうので以下の2つを使う。

// 例外ハンドラを無効化
$this->withoutExceptionHandling();
// AuthorizationExceptionを直接アサート
$this->expectException(AuthorizationException::class);

これ等を使うことで、AuthorizationExceptionを直接拾ってアサートできるという感じ。

結論

以下のように

を付けるとアサート可能。

    /** @test */
    public function cant_update_another_users_post()
    {
        $this->withoutExceptionHandling();
        $this->expectException(AuthorizationException::class);

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

        $post = Post::factory()->for($stranger)->create();

        Livewire::actingAs($user)
            ->test(UpdatePost::class, ['post' => $post])
            ->set('title', 'Living the lavender life')
            ->call('save');
    }

しかし、テスト環境と本番環境で同じ処理になるとすれば、mount()でのauthorizeは適切にレスポンスできないということになりそう。

どちらにしろエラーなので、妥協はできなくはない気もするけど、どうしようかね。