Aturan authorization yang rusak tidak pernah mengeluarkan pesan error — hanya angka status yang berubah: 200 untuk pengguna yang seharusnya ditolak, atau 403 untuk yang seharusnya masuk. Karena satu-satunya gejala kegagalan adalah angka, test harus sistematis: setiap aturan diuji dari kedua sisi (allow dan deny), untuk setiap peran, pada setiap endpoint. Chapter Pengujian Authorization di section Authorization telah memperkenalkan idenya; dari perspektif testing kita sekarang bangun matriks lengkap sebagai data provider PHPUnit, mengetes gate dengan Gate::before hook, dan membuktikan penolakan benar-benar tidak mengubah data.

Bahan Uji: Policy, Gate, dan before Hook #

Suite kita memakai aturan nyata: PostPolicy mengizinkan update untuk pemilik post atau admin, sementara delete hanya untuk pemilik. Sebuah gate lihat-dasbor hanya untuk admin dan super-admin. Di AppServiceProvider, Gate::before memberi super-admin akses ke segalanya:

php
Gate::before(function ($user, $ability) {
    return $user->role === 'super-admin' ? true : null;
});

Mengembalikan true mempercepat izin untuk semua ability; null berarti keputusan diserahkan ke aturan berikutnya — gate atau policy biasa.

Matriks Peran sebagai Data Provider #

Sebelum menulis test, kita petakan ekspektasi ke tabel: siapa pelakunya, aksi apa, status apa yang diharapkan. Sebelas baris berikut menjadi data provider:

php
public static function matriksAkses(): array
{
    return [
        'pemilik memperbarui postnya sendiri' => ['budi', 'perbarui', 200],
        'penulis lain memperbarui post budi' => ['siti', 'perbarui', 403],
        'admin memperbarui post budi' => ['admin', 'perbarui', 200],
        'super-admin memperbarui post budi' => ['super-admin', 'perbarui', 200],
        'pemilik menghapus postnya sendiri' => ['budi', 'hapus', 204],
        'admin menghapus post budi' => ['admin', 'hapus', 403],
        'super-admin menghapus post budi' => ['super-admin', 'hapus', 204],
        'penulis membuka dasbor' => ['siti', 'dasbor', 403],
        'admin membuka dasbor' => ['admin', 'dasbor', 200],
        'super-admin membuka dasbor' => ['super-admin', 'dasbor', 200],
        'tamu memperbarui post budi' => ['tamu', 'perbarui', 401],
    ];
}

Perhatikan dua baris yang sengaja “menolak admin”: policy delete kita memang hanya untuk pemilik, sehingga admin boleh memperbarui (200) tetapi tidak menghapus (403). Baris semacam ini yang membedakan matriks serius dari daftar skenario happy path. Test-nya lalu berjalan untuk setiap baris:

php
#[DataProvider('matriksAkses')]
public function test_matriks_peran_mengikuti_aturan(string $peran, string $aksi, int $status): void
{
    $user = match ($peran) {
        'budi' => $this->postBudi->user,
        'tamu' => null,
        'siti' => $this->user('penulis', '[email protected]'),
        default => $this->user($peran, $peran.'@example.com'),
    };

    $request = $user ? $this->actingAs($user) : $this;

    match ($aksi) {
        'perbarui' => $request->putJson("/posts/{$this->postBudi->id}", [
            'title' => 'Judul Revisi',
        ])->assertStatus($status),
        'hapus' => $request->deleteJson("/posts/{$this->postBudi->id}")
            ->assertStatus($status),
        'dasbor' => $request->get('/dasbor')->assertStatus($status),
    };
}

Endpoint perbarui dan hapus memakai putJson/deleteJson, sehingga tamu menerima 401 (bukan redirect 302 milik request web). Endpoint dasbor memakai middleware can:lihat-dasbor, sedangkan dua lainnya memanggil Gate::authorize() di dalam controller — dua jalur wiring berbeda, sama-sama teruji.

Menguji Policy Tanpa HTTP #

Feature test memverifikasi seluruh jalur, tetapi untuk logika policy itu sendiri kita bisa turun satu lapisan. Method $user->can() dan Gate::forUser() memeriksa aturan tanpa request:

php
$this->assertTrue($budi->can('update', $this->postBudi));
$this->assertFalse($siti->can('update', $this->postBudi));
$this->assertFalse(Gate::forUser($admin)->allows('delete', $this->postBudi));
$this->assertTrue(Gate::forUser($budi)->allows('delete', $this->postBudi));

Empat baris ini adalah unit test bagi PostPolicy. Baris Gate::forUser($admin) membuktikan deny path yang di tabel di atas tampil sebagai 403 — admin memang tidak boleh menghapus. Menguji Gate::before pun tuntas di sini:

php
$this->assertTrue(Gate::forUser($super)->allows('update', $this->postBudi));
$this->assertTrue(Gate::forUser($super)->allows('delete', $this->postBudi));
$this->assertTrue(Gate::forUser($super)->allows('ability-tak-terdefinisi'));

Baris ketiga adalah bunga rampai before hook: super-admin lolos bahkan untuk ability yang tidak pernah kita definisikan — perilaku yang disengaja, tetapi wajib dikunci dengan test agar tidak mengejutkan di kemudian hari.

Penolakan Harus Membekukan Data #

Status 403 saja belum membuktikan keamanan — bisa saja controller menolak setelah menulis. Suite kita menutup celah itu dengan memeriksa database:

php
public function test_penolakan_benar_benar_tidak_mengubah_data(): void
{
    $siti = $this->user('penulis', '[email protected]');

    $this->actingAs($siti)
        ->putJson("/posts/{$this->postBudi->id}", ['title' => 'Serobot Tulisan Budi'])
        ->assertForbidden();

    $this->assertDatabaseHas('posts', [
        'id' => $this->postBudi->id,
        'title' => 'Belajar Otorisasi Laravel',
    ]);
}

Trait RefreshDatabase menjaga setiap baris matriks mulai dari database bersih, sehingga kegagalan satu peran tidak menular ke kasus berikutnya.

Seluruh Suite Berjalan #

bash
php artisan test tests/Feature/OtorisasiMatrixTest.php

Output yang Dihasilkan:

text
  PASS  Tests\Feature\OtorisasiMatrixTest
  ✓  matriks peran mengikuti aturan with data set "pemilik memperbarui p…  0.17s
  ✓  matriks peran mengikuti aturan with data set "penulis lain memperba…  0.01s
  ✓  matriks peran mengikuti aturan with data set "admin memperbarui pos…  0.01s
  ✓  matriks peran mengikuti aturan with data set "super-admin memperbar…  0.01s
  ✓  matriks peran mengikuti aturan with data set "pemilik menghapus pos…  0.01s
  ✓  matriks peran mengikuti aturan with data set "admin menghapus post…  0.01s
  ✓  matriks peran mengikuti aturan with data set "super-admin menghapus…  0.01s
  ✓  matriks peran mengikuti aturan with data set "penulis membuka dasbo…  0.01s
  ✓  matriks peran mengikuti aturan with data set "admin membuka dasbor"  0.01s
  ✓  matriks peran mengikuti aturan with data set "super-admin membuka d…  0.01s
  ✓  matriks peran mengikuti aturan with data set "tamu memperbarui post…  0.01s
  ✓  can dan foruser memeriksa policy tanpa http                         0.01s
  ✓  gate biasa tetap menolak penulis                                    0.01s
  ✓  penolakan benar benar tidak mengubah data                           0.01s

  Tests:    14 passed (24 assertions)
  Duration: 0.38s

Matriks peran kita memakai endpoint JSON untuk memudahkan angka status. Endpoint API itu sendiri punya disiplin assertion yang berbeda — struktur JSON, header, token, dan pagination — yang kita bedah di chapter API Testing berikutnya. Rujukan resminya ada di dokumentasi Testing dan dokumentasi Authorization .