Autentikasi dalam pengujian

Metode actingAs dan pembuatan token API pada pengujian.

Daftar isi

Rute yang dilindungi memiliki dua perilaku yang wajib dibuktikan pada pengujian, yaitu menolak tamu dan melayani pengguna yang sah. Topik ini membahas keduanya pada aplikasi catatan, dimulai dari metode actingAs() untuk mengisi pengguna pada guard web, dilanjutkan dengan pembuatan token API melalui Laravel Sanctum dan pengirimannya melalui withToken(). Rute contoh didefinisikan di dalam kelas tes sehingga setiap kasus dapat direproduksi tanpa menyunting berkas rute aplikasi.

Menolak tamu dan melayani pengguna pada guard web#

Middleware auth memeriksa guard bawaan, yaitu web yang mengenali pengguna dari session. Permintaan web tanpa pengguna melemparkan AuthenticationException dan aplikasi menjawabnya dengan pengalihan ke halaman masuk, sedangkan permintaan JSON dijawab dengan status 401. Tujuan pengalihan bawaan pada Laravel 13 mengarah ke route('login'), sehingga aplikasi tanpa rute bernama login berakhir dengan error 500 alih-alih pengalihan. Pada kelas tes, target pengalihan ditetapkan melalui Authenticate::redirectUsing() agar kasusnya berdiri sendiri tanpa rute tambahan.

Kelas berikut memuat empat kasus yang digunakan sepanjang topik ini. Dua rute uji beserta target pengalihan didefinisikan pada setUp(), sedangkan dua perintah sed menambahkan trait HasApiTokens pada model User apabila belum ada. Trait tersebut diperlukan oleh kasus token pada bagian berikutnya, dan pemeriksaan grep membuat perintah aman dijalankan ulang. Dua kasus tamu dijalankan terlebih dahulu.

bash
# menambahkan trait HasApiTokens pada model User bila belum ada
cd ~/aplikasi-catatan
grep -q 'use HasApiTokens' app/Models/User.php || sed -i 's/use HasFactory, Notifiable;/use HasApiTokens, HasFactory, Notifiable;/' app/Models/User.php
grep -q 'Laravel\\Sanctum\\HasApiTokens' app/Models/User.php || sed -i '/^use Illuminate.Notifications.Notifiable;/a use Laravel\\Sanctum\\HasApiTokens;' app/Models/User.php
# menyiapkan kelas tes autentikasi lalu menjalankan dua kasus tamu
cat > tests/Feature/AutentikasiPengujianTest.php <<'PHP'
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;

class AutentikasiPengujianTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        Authenticate::redirectUsing(fn () => '/login');

        Route::get('/uji/profil', function (Request $request) {
            return ['nama' => $request->user()->name];
        })->middleware('auth');

        Route::get('/uji/profil-api', function (Request $request) {
            return ['nama' => $request->user()->name];
        })->middleware('auth:sanctum');
    }

    public function test_tamu_web_diarahkan_ke_halaman_masuk(): void
    {
        $respons = $this->get('/uji/profil');

        $respons->assertRedirect('/login');
    }

    public function test_tamu_api_menerima_status_unauthorized(): void
    {
        $respons = $this->getJson('/uji/profil-api');

        $respons->assertUnauthorized();
    }

    public function test_actingAs_mengisi_pengguna_pada_guard_web(): void
    {
        $pengguna = User::factory()->create(['name' => 'Ali']);

        $respons = $this->actingAs($pengguna)->get('/uji/profil');

        $respons->assertOk();
        $respons->assertJsonPath('nama', 'Ali');
        $this->assertAuthenticatedAs($pengguna);
    }

    public function test_token_api_menembus_guard_sanctum(): void
    {
        $pengguna = User::factory()->create(['name' => 'Budi']);

        $token = $pengguna->createToken('uji-api');

        $this->assertMatchesRegularExpression('/^\d+\|[A-Za-z0-9]+$/', $token->plainTextToken);

        $respons = $this->withToken($token->plainTextToken)->getJson('/uji/profil-api');

        $respons->assertOk();
        $respons->assertJsonPath('nama', 'Budi');
        $this->assertAuthenticatedAs($pengguna, 'sanctum');
        $this->assertSame(1, $pengguna->tokens()->count());
    }
}
PHP
php artisan test --filter="AutentikasiPengujianTest::test_tamu" | grep -E "PASS|Tests:"
text Keluaran
   PASS  Tests\Feature\AutentikasiPengujianTest
  Tests:    2 passed (3 assertions)

Kasus tamu web dijawab dengan pengalihan menuju /login yang diperiksa assertRedirect(), sedangkan kasus tamu API dijawab dengan status 401 yang diperiksa assertUnauthorized(). Perbedaan kedua jawaban bukan berasal dari middleware yang berbeda, melainkan dari jenis permintaannya. getJson() menandai permintaan sebagai JSON, sehingga pengecualian autentikasi dirender sebagai status 401 alih-alih pengalihan.

actingAs($pengguna)mengisi guard bawaanGuard webidentitas pada sessionTamuredirect /loginwithToken($token)header BearerGuard sanctumtabel personal_access_tokensTamu401 JSON
Dua jalur autentikasi pada pengujian, guard web dan guard sanctum

actingAs untuk mengisi pengguna#

Metode actingAs($pengguna) menempatkan pengguna pada guard yang dituju sebelum permintaan dikirim, dengan guard bawaan web. Pada rute yang dilindungi auth, permintaan berikutnya berperilaku sebagaimana permintaan dari pengguna yang sudah masuk, sehingga $request->user() mengembalikan model yang diberikan. Metode assertAuthenticatedAs() melengkapi pemeriksaan dengan menyatakan guard tersebut memuat pengguna yang sama.

bash
# menjalankan kasus actingAs pada guard web
cd ~/aplikasi-catatan
php artisan test --filter="test_actingAs_mengisi_pengguna_pada_guard_web" | grep -E "PASS|Tests:"
text Keluaran
   PASS  Tests\Feature\AutentikasiPengujianTest
  Tests:    1 passed (5 assertions)

Lima asersi pada kasus ini memeriksa tiga hal, yaitu status respons 200, isi JSON nama yang sesuai, dan keadaan guard setelah permintaan. Nilai name diberikan secara eksplisit pada factory karena UserFactory bawaan menghasilkan nama acak, sehingga tanpa penimpaan nilai, asersi isi JSON tidak dapat menetapkan nilai yang diharapkan.

Perbedaan kedua jalur autentikasi yang dibahas pada topik ini dirangkum pada tabel berikut.

AspekRute web (auth)Rute API (auth:sanctum)
Pembawa identitascookie sessionheader Authorization: Bearer
Helper pengisi identitasactingAs($pengguna)withToken($token->plainTextToken)
Respons untuk tamupengalihan ke /loginstatus 401 JSON

Token API dengan Sanctum#

Laravel Sanctum menangani autentikasi API dengan personal access token, yaitu token yang disimpan pada tabel personal_access_tokens dan dikirim oleh klien pada header Authorization: Bearer. Metode createToken() yang membuat token tersebut berasal dari trait HasApiTokens pada model User. Pada proyek baru, php artisan install:api menambahkan trait itu sekaligus mempublikasikan migration tabelnya, sedangkan blok pertama pada topik ini menambahkan trait yang sama dengan sed karena tabelnya sudah tersedia pada proyek uji.

Kasus berikutnya membuat token melalui createToken('uji-api') lalu mengirim permintaan dengan withToken(). Nilai plainTextToken berbentuk <id>|<deret acak>, misalnya 1|xxxxx… setelah disamarkan, sehingga kasus memeriksa polanya dengan ekspresi reguler alih-alih membandingkan nilainya.

bash
# menjalankan kasus token API pada guard sanctum
cd ~/aplikasi-catatan
php artisan test --filter="test_token_api_menembus_guard_sanctum" | grep -E "PASS|Tests:"
text Keluaran
   PASS  Tests\Feature\AutentikasiPengujianTest
  Tests:    1 passed (7 assertions)

Angka sebelum tanda | pada plainTextToken adalah id baris token di tabel personal_access_tokens. Karena RefreshDatabase memulai setiap kasus dengan tabel kosong, id pertamanya selalu 1. Setelah permintaan diterima, assertAuthenticatedAs($pengguna, 'sanctum') menyatakan guard sanctum mengenali pengguna yang sama, sedangkan hitungan $pengguna->tokens() yang bernilai satu membuktikan token benar-benar tersimpan pada database uji.

Kesalahan umum#

Kesalahan pertama adalah mengharapkan pengalihan halaman masuk pada aplikasi tanpa rute login. Pengalihan tamu bawaan memanggil route('login'), dan tanpa rute bernama tersebut kasus tamu web berhenti dengan error 500 berupa Route [login] not defined alih-alih pengalihan 302. Perbaikannya adalah mendaftarkan rute bernama login pada aplikasi, atau menetapkan Authenticate::redirectUsing() seperti pada contoh.

Kesalahan kedua adalah memanggil createToken() sebelum trait HasApiTokens ditambahkan. Pemanggilan itu berhenti dengan BadMethodCallException: Call to undefined method App\Models\User::createToken() karena metodenya memang belum ada pada model. Trait ditambahkan pada model bersamaan dengan publikasi migration Sanctum melalui php artisan install:api.

Kesalahan ketiga adalah membandingkan plainTextToken dengan nilai tetap pada asersi. Deret acak setelah tanda | berubah pada setiap eksekusi, sehingga pembandingan nilai persis gagal secara tidak menentu. Pola token diperiksa dengan ekspresi reguler, sedangkan kebenaran token dibuktikan oleh respons rute yang menerimanya.

Ringkasan#

  • Tamu pada rute web dijawab pengalihan ke /login oleh middleware auth, sedangkan permintaan JSON tamu dijawab status 401, dan actingAs($pengguna) mengisi guard web sehingga rute terlindungi melayani pengguna tersebut.
  • Token API dibuat melalui createToken() dari trait HasApiTokens, tersimpan pada tabel personal_access_tokens, dan dikirim pada pengujian melalui withToken($token->plainTextToken).
  • Guard sanctum mengenali jalur header Bearer dan juga menerima pengguna session, sehingga kasus token ditulis terpisah apabila jalur token itu sendiri yang dibuktikan.

Referensi#