Aturan authorization adalah kode yang paling berbahaya ketika diam-diam rusak: tidak ada error message, tidak ada stack trace — hanya pengguna yang salah tiba-tiba bisa menghapus data orang lain, atau pengguna yang benar tiba-tiba terkunci. Karena satu-satunya gejala kegagalan adalah angka status yang berubah, feature test adalah alat yang tepat: ia mengirim request sebagai pengguna tertentu lalu memastikan 200 atau 403 keluar persis seperti diperkirakan. Chapter penutup ini menyusun strategi pengujian authorization secara sistematis — matriks peran, pengujian gate dan policy secara langsung tanpa HTTP, hingga data provider untuk menyingkat puluhan kasus.
Matriks Pengujian: Pikirkan Dulu, Tulis Kemudian #
Sebelum menulis test, kita petakan ekspektasi ke dalam matriks: siapa penggunanya, endpoint mana yang dituju, dan status apa yang diharapkan. Untuk aplikasi contoh kita:
| Peran | GET /gate/rahasia | GET /dasbor | PATCH /posts/{post}/judul |
|---|---|---|---|
| penulis (non-pemilik) | 403 | 200 | 403 |
| admin | 200 | 200 | 403 |
| super-admin | 200 | 200 | 200 |
| pemilik post | 403 | 200 | 200 |
Matriks ini memaksa kita jujur pada dua jalur sekaligus: allow path (200) dan deny path (403). Aturan praktisnya — setiap ability yang kita tulis wajib punya minimal satu test untuk masing-masing jalur; test yang hanya memeriksa pengguna yang selalu diizinkan tidak akan menyadari policy yang rusak menjadi terbuka.
actingAs dan assertForbidden #
Bahan dasar feature test authorization adalah helper actingAs() untuk berperan sebagai user tertentu, plus assertion status:
public function test_pemilik_bisa_memperbarui_post(): void
{
$budi = User::factory()->create([
'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$post = Post::create([
'user_id' => $budi->id,
'judul' => 'Belajar Policy Laravel',
'isi' => 'Isi tulisan pertama Budi.',
]);
$response = $this->actingAs($budi)
->putJson("/posts/{$post->id}", ['judul' => 'Judul Baru Budi']);
$response->assertOk();
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'judul' => 'Judul Baru Budi',
]);
}Sisi penolakannya memakai assertForbidden():
$response = $this->actingAs($siti)
->putJson("/posts/{$post->id}", ['judul' => 'Serobot Tulisan Budi']);
$response->assertForbidden();Output yang Dihasilkan (kedua test dijalankan):
pemilik PUT /posts/{post} status: 200
body: {"status":"diperbarui","judul":"Judul Baru Budi"}
siti PUT /posts/{post} status: 403
body: {
"message": "This action is unauthorized."
}Untuk deny path, tambahkan juga assertDatabaseMissing atau assertDatabaseHas seperti di test pemilik — membuktikan data benar-benar tidak berubah, bukan sekadar status ditolak. Trait RefreshDatabase yang melekat pada class test menjaga setiap kasus mulai dari database bersih, sehingga matriks berjalan saling independen: kegagalan satu peran tidak menular ke kasus berikutnya.
Gate::forUser: Menguji Gate Tanpa HTTP #
Feature test melatih jalur penuh, tetapi kadang kita hanya ingin memastikan logika gate itu sendiri. Gate::forUser() memeriksa ability untuk user tertentu tanpa request:
public function test_gate_foruser_tanpa_http(): void
{
$budi = User::factory()->create([
'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$post = Post::create([
'user_id' => $budi->id,
'judul' => 'Uji Gate Langsung',
'isi' => 'Isi tulisan.',
]);
echo 'penulis edit-settings : '
.var_export(Gate::forUser($budi)->allows('edit-settings'), true)."\n";
echo 'penulis touch-post : '
.var_export(Gate::forUser($budi)->allows('touch-post', $post), true)."\n";
echo 'penulis update post : '
.var_export(Gate::forUser($budi)->allows('update', $post), true)."\n";
}Output yang Dihasilkan:
penulis edit-settings : false
penulis touch-post : true
penulis update post : trueBaris ketiga menarik: ability update tidak kita definisikan sebagai gate — ia berasal dari PostPolicy lewat auto-discovery, tetapi Gate::forUser() tetap menemukannya. Inilah bukti bahwa gate dan policy memang satu sistem yang sama.
Menguji Policy Class Secara Langsung #
Policy adalah class PHP biasa, sehingga bisa diuji sebagai objek:
echo '(new PostPolicy)->update($siti, $post) : '
.var_export((new PostPolicy)->update($siti, $post), true)."\n";
echo '(new PostPolicy)->update($budi, $post) : '
.var_export((new PostPolicy)->update($budi, $post), true)."\n";
echo '(new PostPolicy)->delete($siti, $post)->allowed() : '
.var_export((new PostPolicy)->delete($siti, $post)->allowed(), true)."\n";Output yang Dihasilkan:
(new PostPolicy)->update($siti, $post) : false
(new PostPolicy)->update($budi, $post) : true
(new PostPolicy)->delete($siti, $post)->allowed() : falsePengujian langsung ini cepat dan presisi — cocok untuk policy dengan logika cabang rumit. Tetapi ingat: ia tidak menguji wiring (apakah controller benar-benar memanggil policy), jadi kombinasi dengan feature test tetap wajib. Bayangkan lapisannya seperti tangga: unit test pada policy memastikan aturan benar, feature test memastikan aturan terpasang, dan matriks peran memastikan keseluruhan sistem berperilaku seperti tabel desain kita.
Data Provider untuk Matriks #
Matriks dari awal chapter bisa dijalankan sekali jalan lewat data provider PHPUnit — satu method test dikali empat baris data:
public static function matriksAkses(): array
{
return [
'penulis membuka pengaturan' => ['penulis', '/gate/rahasia', 403],
'admin membuka pengaturan' => ['admin', '/gate/rahasia', 200],
'penulis membuka dasbor' => ['penulis', '/dasbor', 200],
'super-admin membuka pengaturan' => ['super-admin', '/gate/rahasia', 200],
];
}
#[DataProvider('matriksAkses')]
public function test_matriks_peran_lolos_prediksi(string $role, string $url, int $status): void
{
$user = User::factory()->create([
'name' => 'Pengguna Uji',
'email' => $role.'@example.com',
'role' => $role,
]);
$response = $this->actingAs($user)->get($url);
$response->assertStatus($status);
}Setiap baris array menjadi satu test tersendiri dengan nama deskriptif; menambah peran baru berarti menambah satu baris, bukan menyalin satu method. Hasil eksekusi tiga berkas test authorization berjalan bersama:
PASS Tests\Feature\AuthorizationMatrixTest
✓ matriks peran lolos prediksi with data set "penulis membuka pengatu… 0.20s
✓ matriks peran lolos prediksi with data set "admin membuka pengatura… 0.01s
✓ matriks peran lolos prediksi with data set "penulis membuka dasbor" 0.01s
✓ matriks peran lolos prediksi with data set "super-admin membuka pen… 0.01s
✓ gate foruser tanpa http 0.01s
PASS Tests\Feature\PostPolicyTest
✓ pemilik bisa memperbarui post 0.01s
✓ pengguna lain ditolak 403 0.01s
PASS Tests\Feature\GateHttpTest
✓ middleware can menolak penulis 0.01s
✓ middleware can mengizinkan admin 0.01s
✓ gate authorize melempar 403 0.01s
✓ dasbor branching gate allows 0.01s
Tests: 11 passed (16 assertions)
Duration: 0.41sDengan ini perjalanan authorization kita lengkap: dari konsep gate dan policy, role dan permission, penerapan di controller, Blade, dan API, sampai proteksi ownership dan pembuktian lewat test. Kerangka yang sama akan terpakai terus di section-section berikutnya, karena hampir semua fitur aplikasi nyata pada akhirnya bertanya: “siapa yang boleh?”.