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:
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:
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:
#[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:
$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:
$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:
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 #
php artisan test tests/Feature/OtorisasiMatrixTest.phpOutput yang Dihasilkan:
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.38sMatriks 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 .