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:

PeranGET /gate/rahasiaGET /dasborPATCH /posts/{post}/judul
penulis (non-pemilik)403200403
admin200200403
super-admin200200200
pemilik post403200200

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:

php
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():

php
$response = $this->actingAs($siti)
    ->putJson("/posts/{$post->id}", ['judul' => 'Serobot Tulisan Budi']);

$response->assertForbidden();

Output yang Dihasilkan (kedua test dijalankan):

text
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:

php
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:

text
penulis edit-settings : false
penulis touch-post    : true
penulis update post   : true

Baris 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:

php
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:

text
(new PostPolicy)->update($siti, $post) : false
(new PostPolicy)->update($budi, $post) : true
(new PostPolicy)->delete($siti, $post)->allowed() : false

Pengujian 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:

php
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:

text
  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.41s

Dengan 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?”.