Files

56 KiB
Raw Permalink Blame History

Dokumentasi Whitebox Testing

Proyek: Laravel Application Security Audit
Tanggal: 16 Mei 2026
Metode: Whitebox Testing (Glass-box / Structural Testing)
Framework: Pest PHP v3 + PHPUnit v11, Laravel 13
Total Test: 371 test, 1.182 assertions — semua LULUS (0 gagal)


Daftar Isi

  1. Pendahuluan
  2. Strategi dan Metodologi
  3. Infrastruktur dan Konfigurasi Test
  4. Unit Tests
  5. Feature Tests — Autentikasi
  6. Feature Tests — API
  7. Feature Tests — Middleware dan Keamanan
  8. Feature Tests — Access Control
  9. Feature Tests — Model dan Database
  10. Feature Tests — Fitur Sistem
  11. Feature Tests — Services
  12. Feature Tests — Performa
  13. Temuan Keamanan dan Perbaikan
  14. Statistik dan Cakupan
  15. Cara Menjalankan Test

1. Pendahuluan

1.1 Apa itu Whitebox Testing?

Whitebox testing (disebut juga glass-box testing atau structural testing) adalah metode pengujian perangkat lunak di mana penguji memiliki akses penuh ke kode sumber, arsitektur internal, alur logika, dan struktur data aplikasi. Berbeda dengan blackbox testing yang hanya menguji perilaku dari luar, whitebox testing menguji bagaimana suatu sistem bekerja dari dalam.

Dalam konteks proyek ini, whitebox testing dilakukan dengan:

  • Membaca seluruh kode sumber controller, middleware, model, dan service
  • Mengidentifikasi setiap jalur eksekusi (code path) yang mungkin terjadi
  • Menulis test case yang secara eksplisit menelusuri jalur-jalur tersebut
  • Memverifikasi bahwa setiap kondisi batas (boundary condition) ditangani dengan benar
  • Memastikan bahwa mekanisme keamanan aktif di setiap lapisan yang relevan

1.2 Tujuan dan Ruang Lingkup

Testing ini dilakukan dalam konteks security audit 3 gelombang yang telah dilakukan pada aplikasi. Tujuan spesifiknya adalah:

Tujuan Keterangan
Verifikasi Session Fixation Memastikan session ID diregenerasi setelah setiap otentikasi
Verifikasi Password Policy Memastikan riwayat, expiry, dan aturan kuat berjalan
Verifikasi Prunable Models Memastikan data lama otomatis terhapus sesuai policy
Verifikasi Access Control Memastikan setiap endpoint dilindungi oleh permission yang tepat
Verifikasi Rate Limiting Memastikan brute-force dilindungi di semua endpoint sensitif
Verifikasi Security Headers Memastikan header HTTP keamanan terpasang di semua respons
Verifikasi XSS Prevention Memastikan output di-escape sebelum dikirim ke client
Verifikasi Cascade Integrity Memastikan penghapusan data tidak menimbulkan orphan records

1.3 Asumsi Dasar

  • Seluruh test dijalankan dalam lingkungan terisolasi (Docker container, database PostgreSQL terpisah)
  • Database di-refresh setiap test menggunakan RefreshDatabase trait
  • Driver session, cache, dan queue semuanya menggunakan array (in-memory) agar test deterministik
  • Test tidak menyentuh sistem produksi, file system nyata (kecuali Storage::fake()), atau layanan eksternal

2. Strategi dan Metodologi

2.1 Pendekatan Pengujian

Whitebox testing pada proyek ini mengikuti pendekatan berlapis:

┌─────────────────────────────────────────────────────┐
│                   UNIT TESTS                        │
│  Menguji fungsi murni tanpa container Laravel       │
│  (formatter, helper, caster, exception factory)     │
├─────────────────────────────────────────────────────┤
│                 FEATURE TESTS                       │
│  Menguji alur end-to-end dengan container penuh     │
│  (HTTP request → middleware → controller → DB)      │
└─────────────────────────────────────────────────────┘

2.2 Pola Test yang Digunakan

Happy Path Testing — jalur sukses normal:

test('password update succeeds with valid current password', function () {
    $user = User::factory()->create(['password' => Hash::make('current-pass')]);
    $this->actingAs($user)->put('/password', [...valid data...])->assertRedirect();
    expect(Hash::check('New-Pass-123', $user->fresh()->password))->toBeTrue();
});

Negative Testing — pengujian kondisi kegagalan yang diharapkan:

test('wrong current password is rejected', function () {
    // Memastikan sistem menolak password yang salah
    $this->actingAs($user)->put('/password', ['current_password' => 'wrong'])
        ->assertSessionHasErrors();
});

Boundary Testing — pengujian nilai batas:

test('upload rejects image exceeding 5 MB', function () {
    $file = UploadedFile::fake()->create('big.jpg', 6000, 'image/jpeg'); // 6 MB
    $this->actingAs($user)->postJson('/editor/upload', ['upload' => $file])->assertStatus(422);
});

Security Path Testing — pengujian jalur keamanan spesifik:

test('web login regenerates the session id', function () {
    $before = session()->getId();
    $this->post('/login', [...]);
    expect(session()->getId())->not->toBe($before); // harus berbeda
});

2.3 Isolasi Test

Setiap test menggunakan RefreshDatabase yang otomatis diterapkan melalui tests/Pest.php. Middleware yang bisa mengganggu test seperti ThrottleRequests dan CheckLegalAgreement dinonaktifkan secara global, kecuali di test yang memang menguji fitur tersebut.


3. Infrastruktur dan Konfigurasi Test

3.1 File Konfigurasi: phpunit.xml

<env name="APP_ENV"              value="testing"  force="true"/>
<env name="BCRYPT_ROUNDS"        value="4"        force="true"/>
<env name="CACHE_STORE"          value="array"    force="true"/>
<env name="SESSION_DRIVER"       value="array"    force="true"/>
<env name="QUEUE_CONNECTION"     value="sync"     force="true"/>
<env name="MAIL_MAILER"          value="array"    force="true"/>
<env name="PULSE_ENABLED"        value="false"    force="true"/>
<env name="TELESCOPE_ENABLED"    value="false"    force="true"/>
<env name="NIGHTWATCH_ENABLED"   value="false"    force="true"/>

Catatan kritis: Atribut force="true" wajib ada agar nilai ini menimpa konfigurasi di .env production yang menggunakan Redis. Tanpa force="true", test akan mencoba konek ke Redis dan gagal karena hostname redis tidak dapat di-resolve di luar container Docker.

3.2 File Bootstrap: tests/Pest.php

uses(TestCase::class, RefreshDatabase::class)
    ->in('Feature');                              // Semua Feature test refresh DB

uses(TestCase::class)
    ->in('Unit');                                 // Unit test tidak butuh DB

expect()->extend('toBeOne', fn () => $this->toBe(1));

// Nonaktifkan throttle dan legal check secara global
\Illuminate\Support\Facades\Route::middlewareGroup('web', [...]);

Dua middleware dinonaktifkan global:

  • ThrottleRequests — dinonaktifkan agar test tidak saling mengganggu; ada file test khusus yang mengaktifkan kembali
  • CheckLegalAgreement — dinonaktifkan agar sebagian besar test tidak perlu setup persetujuan hukum

3.3 Struktur Direktori Test

tests/
├── Pest.php                          ← Bootstrap + trait global
├── TestCase.php                      ← Base class
├── Unit/
│   ├── ExampleTest.php
│   ├── Exceptions/
│   │   └── CustomExceptionsTest.php
│   ├── Helpers/
│   │   └── SessionHelperTest.php
│   ├── Monitoring/
│   │   └── MonitoringFormatterTest.php
│   ├── System/
│   │   └── ActivityFormatterTest.php
│   └── SystemConfig/
│       └── SettingValueCasterTest.php
└── Feature/
    ├── AccessControl/
    │   ├── PermissionManagementTest.php
    │   ├── RoleManagementTest.php
    │   └── UserManagementTest.php
    ├── Api/
    │   ├── ApiAuthExtendedTest.php
    │   ├── AuthTest.php
    │   ├── DeviceTokenTest.php
    │   ├── HealthTest.php
    │   └── OtpTest.php
    ├── Auth/
    │   ├── AuthenticationTest.php
    │   ├── EmailVerificationTest.php
    │   ├── PasswordConfirmationTest.php
    │   ├── PasswordControllerTest.php   ← BARU (whitebox)
    │   ├── PasswordResetTest.php
    │   ├── PasswordUpdateTest.php
    │   ├── RegistrationTest.php
    │   ├── SessionFixationTest.php      ← BARU (whitebox)
    │   ├── SocialAuthTest.php
    │   ├── TwoFactorTest.php
    │   └── WebAuthnConfigTest.php
    ├── Database/
    │   └── CascadeIntegrityTest.php
    ├── Helpers/
    │   ├── ApiResponseTest.php
    │   └── PasswordRuleHelperTest.php
    ├── Middleware/
    │   ├── CheckActivePermissionTest.php
    │   ├── CheckLegalAgreementTest.php
    │   ├── IpAccessControlTest.php
    │   ├── PasswordExpiryMiddlewareTest.php
    │   └── SecurityHeadersTest.php
    ├── Models/
    │   └── PrunableModelsTest.php       ← BARU (whitebox)
    ├── Performance/
    │   └── NPlusOneTest.php
    ├── Services/
    │   ├── Auth/
    │   │   └── PasswordPolicyServiceTest.php
    │   ├── System/
    │   │   └── BackupManagementServiceTest.php
    │   └── SystemConfig/
    │       └── SystemConfigServiceTest.php
    ├── System/
    │   ├── AiCircuitBreakerTest.php
    │   ├── EditorUploadTest.php         ← BARU (whitebox)
    │   ├── NotificationCenterTest.php   ← BARU (whitebox)
    │   └── SessionManagerTest.php       ← BARU (whitebox)
    ├── ExampleTest.php
    ├── ImpersonateTest.php
    ├── MobileConfigTest.php
    ├── ProfileTest.php
    └── RateLimitTest.php

4. Unit Tests

Unit tests menguji komponen secara terisolasi tanpa men-boot container Laravel. Semua fungsi yang diuji adalah pure functions atau kelas yang tidak bergantung pada database.

4.1 SessionHelperTest.php — Parser User-Agent

File: tests/Unit/Helpers/SessionHelperTest.php
Target: app/Helpers/SessionHelper.phpparseUserAgent()

Fungsi ini digunakan di Session Manager untuk menampilkan informasi perangkat pengguna. Pengujian memverifikasi akurasi deteksi sistem operasi dan browser.

Test Case Input Expected Output
null user agent null "Unknown"
string kosong "" "Unknown"
Deteksi Android "...Android..." Platform: Android
Deteksi iOS "...iPhone..." Platform: iOS
Deteksi Windows "...Windows NT..." Platform: Windows
Deteksi macOS "...Macintosh..." Platform: macOS
Deteksi Linux "...Linux..." Platform: Linux
Edge sebelum Chrome "...Edg/..." Browser: Edge (bukan Chrome)
Chrome "...Chrome/..." Browser: Chrome
Firefox "...Firefox/..." Browser: Firefox
Safari "...Safari/..." (tanpa Chrome) Browser: Safari
Browser tidak dikenal "BotAgent/1.0" Icon: bi-globe

Mengapa penting: Deteksi Edge harus diperiksa sebelum Chrome karena user-agent Edge mengandung string "Chrome". Tanpa urutan yang benar, semua pengguna Edge akan terdeteksi sebagai Chrome.

4.2 SettingValueCasterTest.php — Casting Nilai Konfigurasi

File: tests/Unit/SystemConfig/SettingValueCasterTest.php
Target: app/Services/SystemConfig/SettingValueCaster.php

Menguji serialisasi dan deserialisasi nilai pengaturan sistem untuk berbagai tipe data.

Tipe yang diuji:

Tipe Input Setelah normalize() Setelah serialize() Setelah deserialize()
bool "true" true "1" true
int "42" 42 "42" 42
float "3.14" 3.14 "3.14" 3.14
json '{"a":1}' ["a" => 1] '{"a":1}' ["a" => 1]
string " hello " "hello" "hello" "hello"
image_path /storage/img.png path path path

Test isUnchanged() memverifikasi bahwa nilai yang identik dengan yang tersimpan tidak dianggap berubah, mencegah penulisan revision yang tidak perlu.

4.3 ActivityFormatterTest.php — Format Log Aktivitas

File: tests/Unit/System/ActivityFormatterTest.php
Target: app/Services/System/ActivityFormatter.php

Menguji logika format tampilan di audit log.

Test getFriendlyModelName():

  • null"System"
  • "App\Models\User""User"
  • Class tidak dikenal → headline dari basename

Test getEventBadgeClass():

Event CSS Class
created text-bg-success
updated text-bg-warning
deleted text-bg-danger
restored text-bg-info
login text-bg-info
logout text-bg-secondary
password_changed text-bg-primary
event tidak dikenal text-bg-theme-1

Test juga memverifikasi bahwa fungsi case-insensitive (input "LOGIN" sama dengan "login").

Test formatChanges():

  • Field sensitif (password, remember_token) disembunyikan dari output
  • Nilai boolean ditampilkan sebagai "Ya" / "Tidak" dalam bahasa Indonesia
  • Array nested diubah menjadi string yang dapat dibaca manusia

4.4 CustomExceptionsTest.php — Custom Exception

File: tests/Unit/Exceptions/CustomExceptionsTest.php
Target: app/Exceptions/

Memverifikasi bahwa factory method pada custom exception menghasilkan pesan yang terstruktur dan jenis exception yang benar.

Exception Class Factory Methods yang Diuji
SystemConfigException invalidKey(), saveFailed(), cacheFailed()
BackupOperationException notFound(), restoreFailed(), createFailed()
MonitoringException serviceUnavailable(), dataCollectionFailed()

Semua exception memverifikasi bahwa mereka adalah turunan RuntimeException.

4.5 MonitoringFormatterTest.php — Format Monitoring

File: tests/Unit/Monitoring/MonitoringFormatterTest.php
Target: app/Services/Monitoring/MonitoringFormatter.php

Fungsi Test Cases
bytes() B, KB, MB, GB, TB; presisi; nilai negatif di-clamp ke 0
duration() < 1 menit, format menit, jam+menit, hari+jam+menit; lewati komponen nol
parseBytes() Round-trip unit penuh, string numerik, unit tidak dikenal, nilai fraksional

5. Feature Tests — Autentikasi

5.1 AuthenticationTest.php — Login/Logout Dasar

File: tests/Feature/Auth/AuthenticationTest.php

Test Case Verifikasi
Halaman login tersedia HTTP 200, tampilan form login
Login berhasil dengan kredensial valid Redirect ke dashboard, user terautentikasi
Login gagal dengan password salah Session error, user tidak terautentikasi
Logout berhasil Session dihancurkan, redirect ke login

5.2 PasswordControllerTest.php — Update Password Web (BARU)

File: tests/Feature/Auth/PasswordControllerTest.php
Target: app/Http/Controllers/Auth/PasswordController.php
Endpoint: PUT /password

File ini ditulis khusus sebagai bagian dari whitebox testing untuk menguji semua jalur kode di PasswordController::update().

Test 1: Happy Path

Input: current_password benar, password baru valid, konfirmasi cocok
Expected: Redirect ke /profile, tanpa error di session
Verifikasi: Hash::check(password_baru, user->fresh()->password) === true

Test 2: Stamping Timestamp

Setup: password_changed_at = null
Input: password update berhasil
Verifikasi: password_changed_at TIDAK NULL setelah update

Memastikan PasswordPolicyService::recordPasswordChange() dipanggil dan menyimpan timestamp.

Test 3: Validasi — Password Lama Salah

Input: current_password = 'wrong-pass'
Expected: Session error di bag 'updatePassword', field 'current_password'
Verifikasi: Password di database TIDAK berubah

Test 4: Validasi — Konfirmasi Tidak Cocok

Input: password = 'ABC', password_confirmation = 'XYZ'
Expected: Session errors (validasi gagal)

Test 5: Penolakan Riwayat Password

Setup: password_history_count = 3, user punya riwayat 'OldPassword1!'
Input: Mencoba menggunakan kembali 'OldPassword1!'
Expected: Session error (password sudah pernah digunakan)

Test 6: Bypass Riwayat Jika Dinonaktifkan

Setup: password_history_count = 0
Input: Menggunakan kembali password lama
Expected: Berhasil (history check di-skip)

Membuktikan bahwa pengecekan riwayat bersyarat, bukan selalu aktif.

Test 7: Pencatatan Riwayat

Setup: password_history_count = 5
Input: Password update berhasil
Verifikasi: PasswordHistory::where('user_id', $user->id)->count() === 1

Test 8: Password Lama Tidak Bisa Login

Input: Update password dari 'old-pass-456' ke 'Brand-New-888!'
Verifikasi:
  Hash::check('old-pass-456', user->fresh()->password) === false
  Hash::check('Brand-New-888!', user->fresh()->password) === true

Test ini menggantikan uji logoutOtherDevices yang lebih kompleks — fokus pada hasil fungsional yang paling penting: password lama benar-benar tidak valid.

Test 9: Guest Ditolak

Input: PUT /password tanpa autentikasi
Expected: Redirect ke /login (HTTP 302)

Test 10: Respons JSON

Input: putJson('/password', [...]) dengan credentials valid
Expected: HTTP 200, JSON body {success: true}

Membuktikan controller menangani JSON request dengan benar.

5.3 SessionFixationTest.php — Pencegahan Session Fixation (BARU)

File: tests/Feature/Auth/SessionFixationTest.php

Session fixation adalah serangan di mana penyerang menanamkan ID session sebelum korban login, lalu menggunakan ID yang sama setelah korban login untuk membajak session. Pencegahan dilakukan dengan meregenerasi session ID setelah setiap otentikasi.

Test 1: Login Web

Sebelum: session_id = "abc123"
Action:  POST /login dengan kredensial valid
Setelah: session_id ≠ "abc123"

Test 2: Verifikasi 2FA

Setup: Session berisi 2fa_user_id, 2fa_code, 2fa_expires_at
Sebelum: session_id = X
Action:  POST /2fa dengan code benar
Setelah: session_id ≠ X

Penting: tanpa regenerasi ini, penyerang yang mengetahui session pre-2FA bisa membypass 2FA.

Test 3: OAuth Callback (SocialAuth)

Setup: feature_google_oauth = true, session berisi social_auth_provider
Action: GET /auth/callback (Socialite di-mock mengembalikan user valid)
Verifikasi: session_id berubah

Perbaikan ini ditambahkan ke SocialAuthController::callback() selama security audit.

Test 4: Reset Password

Action: POST /reset-password dengan token valid
Verifikasi: session_id berubah setelah reset

Test 5: Mulai Impersonasi

Action: Admin memulai impersonasi user lain
Verifikasi: session_id berubah (session admin terisolasi dari session target)

Test 6: Berhenti Impersonasi

Action: POST /impersonate/stop
Verifikasi: session_id berubah (kembali ke konteks admin)

5.4 SocialAuthTest.php — OAuth Authentication

File: tests/Feature/Auth/SocialAuthTest.php
Target: app/Http/Controllers/Auth/SocialAuthController.php

Test Case Skenario Keamanan Expected
Provider dinonaktifkan feature_google_oauth = false 404 Not Found
Provider diaktifkan feature_google_oauth = true Redirect ke Google
Tanpa sesi provider Tidak ada social_auth_provider di session Redirect ke login + error
Email belum diverifikasi email_verified = false dari provider Ditolak, tidak login
User baru Email belum ada di DB User baru dibuat, diberi role User
Link email User ada, belum punya google_id google_id terhubung
Takeover dicegah Email ada, google_id BERBEDA Ditolak — identity existing user tidak bisa ditimpa
Re-use by provider ID User sudah punya google_id yang cocok Login langsung
Exception dari Socialite driver->user() throw exception Redirect ke login + error

Kasus kritis — Identity Takeover Prevention:

// Penyerang punya email yang sama dengan user lain, tapi google_id berbeda
$existing = User::factory()->create([
    'email' => 'taken@example.com',
    'google_id' => 'different-google-id',  // sudah ada
]);
// Socialite mengembalikan: id='attacker-id', email='taken@example.com'
// Sistem HARUS menolak — tidak boleh menimpa google_id yang sudah ada
$this->get('/auth/callback')
    ->assertRedirect('/login')
    ->assertSessionHas('error');
expect($existing->fresh()->google_id)->toBe('different-google-id'); // tidak berubah

5.5 TwoFactorTest.php — Autentikasi Dua Faktor

File: tests/Feature/Auth/TwoFactorTest.php
Target: 2FA Controller

Test Case Verifikasi
View 2FA butuh session Tanpa auth.2fa_user_id → redirect
Kode benar → login User terautentikasi setelah kode cocok
Kode salah → ditolak User tetap tidak terautentikasi
Kode kadaluarsa → redirect Diarahkan kembali ke login
Kode terlalu pendek Validasi menolak (< 6 digit)
Trust device: simpan Row di user_trusted_devices terbuat
Trust device: tidak dipilih Tidak ada row trusted device
Cookie device trusted Skip 2FA, langsung login
Cookie device salah secret 2FA tetap diminta

5.6 PasswordResetTest.php — Reset Password via Email

File: tests/Feature/Auth/PasswordResetTest.php

Test Case Verifikasi
Halaman request reset tersedia HTTP 200
Request token berhasil Email dikirim (Notification::fake)
Reset dengan token valid Password diubah, redirect ke login

5.7 RegistrationTest.php — Registrasi User

File: tests/Feature/Auth/RegistrationTest.php

Test Case Verifikasi
Halaman registrasi tersedia HTTP 200
Registrasi berhasil User dibuat di DB, role User diberikan

5.8 WebAuthnConfigTest.php — Konfigurasi WebAuthn

File: tests/Feature/Auth/WebAuthnConfigTest.php

Test struktural yang memverifikasi:

  • Class controller WebAuthn login dan register ada
  • Method yang dibutuhkan tersedia
  • Setting webauthn_enabled dapat di-toggle
  • Tabel webauthn_credentials ada dan memiliki kolom yang benar

6. Feature Tests — API

6.1 AuthTest.php — API Authentication Dasar

File: tests/Feature/Api/AuthTest.php
Base URL: /api/v1/

Endpoint Test Case Kode Respons
POST /register Registrasi sukses 201 Created
POST /register Email duplikat 422 Unprocessable
POST /login Credentials valid 200 OK + token
POST /login Password salah 422 Unprocessable
POST /login User tidak aktif 422 Unprocessable
POST /logout Logout dengan token 200 OK
GET /profile Profil user sendiri 200 OK + data user

6.2 ApiAuthExtendedTest.php — API Auth Lanjutan (BARU)

File: tests/Feature/Api/ApiAuthExtendedTest.php

File ini ditulis sebagai bagian whitebox testing untuk menguji endpoint yang lebih kompleks dari API AuthController.

Grup: Delete Account (DELETE /api/v1/profile/delete)

Test Case Input Expected
Hapus akun berhasil password benar 200, user terhapus dari DB
Password salah password salah 422, user tetap ada
Password tidak dikirim body kosong 422 Validation Error
Guest tidak bisa akses tanpa token 401 Unauthorized
Semua token ikut dicabut delete akun user->tokens()->count() === 0

Verifikasi pencabutan token:

$user->createToken('device-a');
$user->createToken('device-b');
$token = $user->createToken('device-c')->plainTextToken;
$this->withHeader('Authorization', "Bearer {$token}")
    ->deleteJson('/api/v1/profile/delete', ['password' => 'secret']);
expect($user->tokens()->count())->toBe(0); // semua token dicabut

Grup: Update Password API (POST /api/v1/profile/password)

Test Case Verifikasi
Update berhasil Hash::check(password_baru) === true
Current password salah 422
Reuse password dari riwayat 422, body: {status: "error"}
Update berhasil → catat riwayat PasswordHistory::count() === 1

Grup: Register dengan Password Policy

Test Case Setting Expected
Min length tidak terpenuhi password_min_length = 12, kirim 5 char 422
Min length terpenuhi password_min_length = 6, kirim 10 char 201
Harus ada angka password_require_numeric = true 422 jika tanpa angka

Grup: Update Profil (POST /api/v1/profile/update)

Test Case Verifikasi
Update nama berhasil user->fresh()->name === 'Updated Name'
Nama > 255 karakter 422 Validation Error

6.3 OtpTest.php — OTP Code

File: tests/Feature/Api/OtpTest.php

Test Case Verifikasi
Kirim OTP (email valid) Mail diantri, response 200
Verifikasi OTP salah 422
Kode OTP < 6 digit 422 Validation Error

6.4 HealthTest.php — Health Check

File: tests/Feature/Api/HealthTest.php

Test Case Verifikasi
GET /api/v1/health 200 OK, struktur respons valid
Respons berisi timestamp created_at ada di body
Setiap check punya status Array checks dengan field status

6.5 DeviceTokenTest.php — Push Notification Token

File: tests/Feature/Api/DeviceTokenTest.php

Test Case Verifikasi
Guest tidak bisa registrasi 401 Unauthorized
User bisa registrasi token 200, token tersimpan
Token duplikat → upsert Tidak ada error, tidak ada duplikat
Unregister token Token terhapus dari DB

7. Feature Tests — Middleware dan Keamanan

7.1 SecurityHeadersTest.php — HTTP Security Headers

File: tests/Feature/Middleware/SecurityHeadersTest.php

Setiap request web harus menghasilkan header keamanan yang benar. Test ini membuat route khusus /__sec-probe dan memeriksa semua header respons.

Header Nilai yang Diharapkan Tujuan
X-Content-Type-Options nosniff Mencegah MIME sniffing
X-Frame-Options SAMEORIGIN Mencegah clickjacking
Referrer-Policy strict-origin-when-cross-origin Batasi referrer info
Permissions-Policy camera=(), microphone=(), geolocation=() Batasi akses API browser
X-XSS-Protection tidak null Perlindungan XSS browser lama
Strict-Transport-Security null (over HTTP) HSTS hanya aktif di HTTPS

Catatan penting: HSTS sengaja tidak dikirim saat request melalui HTTP biasa (seperti dalam environment test). Ini adalah perilaku yang benar — HSTS di HTTP tidak ada gunanya dan bisa menyebabkan masalah jika dikonfigurasi salah.

7.2 IpAccessControlTest.php — Kontrol Akses IP

File: tests/Feature/Middleware/IpAccessControlTest.php
Target: app/Http/Middleware/IpAccessControl.php

Test Case Setting IP Request Expected
Tanpa aturan - 127.0.0.1 200 OK
IP di blacklist ip_blacklist = "127.0.0.1" 127.0.0.1 403 Forbidden
IP tidak di blacklist ip_blacklist = "10.0.0.5" 127.0.0.1 200 OK
Admin route, IP tidak di whitelist ip_whitelist_admin = "203.0.113.1" 127.0.0.1 403 Forbidden
Admin route, IP di whitelist ip_whitelist_admin = "127.0.0.1" 127.0.0.1 200 OK
Non-admin route, whitelist admin ada ip_whitelist_admin = "203.0.113.1" 127.0.0.1 200 OK
IP di auto-block cache ip_block:127.0.0.1 di cache 127.0.0.1 429
Single session: stale session session_single_session = true, last_session_id berbeda - Redirect ke login

Detail test single session:

$user = User::factory()->create(['last_session_id' => 'OTHER_SESSION_ID']);
$this->actingAs($user)->get('/__ip-probe')
    ->assertRedirect(route('login', absolute: false));
$this->assertGuest(); // user sudah di-logout

7.3 RateLimitTest.php — Rate Limiting

File: tests/Feature/RateLimitTest.php

Catatan penting: Middleware ThrottleRequests dinonaktifkan secara global di Pest.php. File ini secara eksplisit mengaktifkan kembali middleware tersebut dengan $this->withMiddleware(ThrottleRequests::class).

Endpoint Test Kondisi Block
POST /api/v1/login 15 percobaan Salah satu harus menghasilkan 429
POST /api/v1/forgot-password 5 percobaan Request ke-6 → 429
POST /api/v1/otp/send 5 percobaan Request ke-6 → 429
POST /api/v1/register 5 percobaan Request ke-6 → 429
POST /2fa 5 percobaan Request ke-6 → 429

Test isolasi IP:

// IP A sudah memenuhi batas
for ($i = 0; $i < 5; $i++) {
    $this->call('POST', '/api/v1/forgot-password', server: ['REMOTE_ADDR' => '10.0.0.1']);
}
// IP B berbeda — tidak boleh kena blok
$r = $this->call('POST', '/api/v1/forgot-password', server: ['REMOTE_ADDR' => '10.0.0.2']);
expect($r->getStatusCode())->not->toBe(429);

7.4 CheckActivePermissionTest.php — Permission Aktif

File: tests/Feature/Middleware/CheckActivePermissionTest.php

Middleware ini memverifikasi bahwa permission yang dimiliki user masih dalam status is_active = true. Digunakan sebagai lapisan kontrol tambahan di luar Spatie Permission.

Test Case Status Permission Expected
Permission tidak aktif is_active = false 403 Forbidden
Permission aktif is_active = true 200 OK
Permission tidak ada - 403 Forbidden
Status dicache - Hasil dicache, tidak query ulang

7.5 CheckLegalAgreementTest.php — Persetujuan Hukum

File: tests/Feature/Middleware/CheckLegalAgreementTest.php

Test Case Kondisi Expected
Guest Tidak terautentikasi Tidak terpengaruh (lewat)
User tanpa consent Belum pernah setuju Redirect ke halaman consent
User dengan consent terkini Sudah setuju versi terbaru 200 OK
User dengan consent lama Versi policy sudah diupdate Redirect ke consent baru

7.6 PasswordExpiryMiddlewareTest.php — Kadaluarsa Password

File: tests/Feature/Middleware/PasswordExpiryMiddlewareTest.php

Test Case Kondisi Expected
Password baru password_changed_at baru 200 OK, tidak redirect
Password kadaluarsa password_changed_at > batas Redirect ke halaman update password
Fitur dinonaktifkan password_expiry_days = 0 Tidak pernah redirect
Guest Tidak terautentikasi Tidak terpengaruh

8. Feature Tests — Access Control

8.1 UserManagementTest.php — Manajemen User

File: tests/Feature/AccessControl/UserManagementTest.php

Test Case Verifikasi
Guest tidak bisa akses Redirect ke login
User tanpa izin diblokir 403 Forbidden
Buat user berhasil User ada di DB dengan role
Password user baru divalidasi Aturan kuat diterapkan
Update nama Nama berubah
Update role Role berubah
Update password saja Hanya password yang berubah
Toggle status aktif is_active berpindah true/false
Soft delete User ditandai deleted_at
Force delete User dihapus permanen
Force delete dengan referensi Tidak bisa jika masih ada FK aktif (sesuai aturan bisnis)

8.2 RoleManagementTest.php — Manajemen Role

File: tests/Feature/AccessControl/RoleManagementTest.php

Test Case Verifikasi
Buat role + permissions Role dan relasi tersimpan
Nama duplikat ditolak 422 Validation Error
Permission invalid 422
Karakter tidak valid di nama 422
Update role Nama dan permissions berubah
Toggle status is_active berubah
Soft delete (ada user di role) Ditolak dengan pesan error
Hard delete (tidak ada user) Role dihapus
Restore soft-deleted Role kembali aktif

8.3 PermissionManagementTest.php — Manajemen Permission

File: tests/Feature/AccessControl/PermissionManagementTest.php

Test Case Verifikasi
Buat permission Tersimpan di DB
Cross-guard support Permission bisa untuk guard berbeda
Nama duplikat dalam guard 422
Guard tidak valid 422
Karakter tidak valid 422
Update nama Nama berubah
Toggle status is_active berubah
Soft delete deleted_at diset

9. Feature Tests — Model dan Database

9.1 PrunableModelsTest.php — Prunable Models (BARU)

File: tests/Feature/Models/PrunableModelsTest.php

Trait MassPrunable dari Laravel memungkinkan model mendefinisikan metode prunable() yang mengembalikan query untuk record yang harus dihapus saat php artisan model:prune dijalankan. Test ini memverifikasi bahwa setiap model memprune record yang tepat.

Catatan teknis: File ini awalnya ada di tests/Unit/Models/ tetapi dipindahkan ke tests/Feature/Models/ karena query Eloquent membutuhkan database dan container Laravel penuh.

Model Tabel Kriteria Prune Diuji
OtpCode otp_codes expires_at < now() Record kadaluarsa dipilih, valid tidak dipilih
UserTrustedDevice user_trusted_devices expires_at < now() Device expired dipilih
PasswordHistory password_histories created_at < 365 hari lalu Record > 1 tahun dipilih
MobileSyncLog mobile_sync_logs synced_at < 30 hari lalu Record > 30 hari dipilih
MobileErrorLog mobile_error_logs occurred_at < 90 hari lalu Record > 90 hari dipilih
Notification system_notifications created_at < 30 hari lalu Notifikasi lama dipilih
AiHealingLog ai_healing_logs created_at < 90 hari lalu Log lama dipilih
AiUsageLog ai_usage_logs created_at < 3 bulan lalu Log > 3 bulan dipilih

Pola test per model:

// 1. Insert record yang HARUS di-prune
// 2. Insert record yang TIDAK BOLEH di-prune
// 3. Panggil prunable() dan periksa hasil
test('MobileErrorLog prunable selects records older than 90 days', function () {
    \DB::table('mobile_error_logs')->insert([
        ['message' => 'old error', 'occurred_at' => now()->subDays(100)], // HARUS dipilih
        ['message' => 'new error', 'occurred_at' => now()->subDays(5)],   // TIDAK boleh
    ]);
    $ids = (new MobileErrorLog)->prunable()->pluck('id');
    expect($ids)->toHaveCount(1);
});

Edge cases yang diuji:

  • OtpCode: query mengembalikan empty collection saat semua kode masih valid
  • UserTrustedDevice: query mengembalikan empty saat tidak ada device expired

Perbaikan kolom yang ditemukan selama whitebox testing:

Model Kolom Salah (awal) Kolom Benar
OtpCode email identifier
MobileSyncLog device_id, created_at, updated_at Hanya synced_at
MobileErrorLog level, created_at, updated_at message, occurred_at

9.2 CascadeIntegrityTest.php — Integritas Cascade

File: tests/Feature/Database/CascadeIntegrityTest.php

Memverifikasi bahwa foreign key constraints dan cascade rules di database berfungsi dengan benar.

Test Case Aksi Expected Cascade
Force delete user $user->forceDelete() password_histories dihapus
Force delete user $user->forceDelete() user_consents dihapus
Force delete user $user->forceDelete() user_trusted_devices dihapus
Force delete user $user->forceDelete() system_settings.created_by/updated_by → null
Force delete user $user->forceDelete() system_setting_revisions.changed_by → null
Delete role $role->forceDelete() role_has_permissions dihapus
Delete permission $perm->forceDelete() role_has_permissions dihapus
Force delete user (audit role) $actor->forceDelete() roles.created_by/updated_by → null
Soft delete user $user->delete() Related rows TETAP ada (tidak cascade)

Mengapa penting: Test ini membuktikan bahwa:

  1. Hard delete tidak meninggalkan orphan records (data yatim)
  2. Audit columns di-nullify ketika actor dihapus (tidak ada FK broken)
  3. Soft delete tidak menghapus related records (intended behavior)

10. Feature Tests — Fitur Sistem

10.1 SessionManagerTest.php — Manajemen Session (BARU)

File: tests/Feature/System/SessionManagerTest.php
Target: app/Http/Controllers/SystemSettings/SessionManagerController.php
Endpoint: /session-manager

Kontrol Akses

Test Case Expected
Guest GET /session-manager Redirect ke /login
User tanpa izin 403 Forbidden
User dengan view active sessions 200 OK
Guest GET /session-manager/stats (JSON) 401 Unauthorized (bukan redirect)
User viewer GET /session-manager/stats 200 OK + {total, active}

Catatan: JSON request dari guest mengembalikan 401, bukan 302. Ini adalah perilaku standar Laravel untuk request dengan header Accept: application/json.

Statistik Struktur

test('stats endpoint returns valid json with total key', function () {
    $this->actingAs(makeSessionViewer())
        ->getJson('/session-manager/stats')
        ->assertOk()
        ->assertJsonStructure(['total', 'active']);
});

Terminasi Session

Test Case Expected
User tanpa manage active sessions 403 Forbidden
Manager mencoba terminasi session sendiri 403 Forbidden (dikecualikan)
Manager terminasi session lain 200 OK, {success: true}
Guest terminasi session 401 Unauthorized

Test "tidak bisa terminasi session sendiri" menggunakan teknik khusus:

test('manager cannot terminate their own current session', function () {
    $manager = makeSessionManager();
    $this->actingAs($manager);

    // Panggil controller langsung — HTTP request baru akan punya session ID berbeda
    $controller = app(\App\Http\Controllers\SystemSettings\SessionManagerController::class);
    $currentId  = session()->getId();

    $response = $controller->destroy(request(), $currentId);

    expect($response->getStatusCode())->toBe(403);
    expect(json_decode($response->getContent(), true)['success'])->toBeFalse();
});

Mengapa tidak via HTTP? Karena driver session array membuat ID baru di setiap HTTP request test. Memanggil controller langsung via app() mempertahankan ID session yang sama sehingga kondisi "session sendiri" dapat diuji.

10.2 EditorUploadTest.php — Upload Gambar CKEditor (BARU)

File: tests/Feature/System/EditorUploadTest.php
Target: app/Http/Controllers/SystemSettings/EditorUploadController.php
Endpoint: POST /editor/upload

Setup: Storage::fake('public') digunakan agar file tidak benar-benar disimpan ke disk.

Kontrol Akses

Test Case Expected
Guest upload 401 Unauthorized
User tanpa manage global settings 403 Forbidden

Validasi File

Test Case Input Expected
Tanpa file Body kosong 400, {error.message: "No file uploaded."}
File PHP shell.php (mime: application/x-php) 422
File SVG vector.svg (mime: image/svg+xml) 422 (SVG bisa mengandung script)
File JPEG valid photo.jpg 200, {uploaded: 1, url, fileName}
File PNG valid icon.png 200, {uploaded: 1}
File WebP valid modern.webp 200, {uploaded: 1}
File > 5 MB 6000 KB JPEG 422

Mengapa SVG ditolak? SVG adalah format XML yang dapat mengandung script <script> tag dan event handler XSS. Menerima SVG dari pengguna membuka vektor serangan XSS yang signifikan.

Verifikasi Penyimpanan

test('uploaded file is persisted to the public disk under editor/', function () {
    $file = UploadedFile::fake()->image('stored.jpg');
    $response = $this->actingAs($user)->post('/editor/upload', ['upload' => $file])->json();

    // Ekstrak path relatif dengan benar menggunakan substr
    $relativePath = substr((string) $response['url'], strlen('/storage/'));
    Storage::disk('public')->assertExists($relativePath);
});

Catatan bug yang ditemukan: Penggunaan ltrim($url, '/storage/') salah karena ltrim menghapus karakter satu per satu, bukan prefix string. Misalnya, ltrim('/storage/editor/file.jpg', '/storage/') menghasilkan ditor/... karena karakter 'e' juga ada di daftar karakter yang dihapus. Diperbaiki menggunakan substr($url, strlen('/storage/')).

10.3 NotificationCenterTest.php — Pusat Notifikasi (BARU)

File: tests/Feature/System/NotificationCenterTest.php
Target: NotificationCenterController.php
Endpoint: /notification-center

Kontrol Akses

Test Case Expected
Guest GET /notification-center Redirect ke /login
User tanpa permission 403 Forbidden
User dengan view notification center 200 OK

Broadcast Notifikasi (POST /notification-center)

Test Case Expected
Viewer tanpa manage notification center 403 Forbidden
Admin broadcast (judul, pesan, recipient, type) 200, {success: true}, notif tersimpan di DB
Tipe notifikasi tidak valid (xss-alert) 422 Validation Error
Tanpa judul dan pesan 422 Validation Error

Tanda Sudah Dibaca

Test Case Expected
User tandai notifikasi sebagai dibaca 200, {success: true}
Guest tandai sebagai dibaca (JSON request) 401 Unauthorized
Tandai semua sebagai dibaca 200, {success: true}

Hapus Personal (Hide)

test('user can delete (hide) a notification from their view', function () {
    $user = makeNotificationViewer();
    $notification = makeNotificationForUser($user);
    $this->actingAs($user)
        ->deleteJson(route('notification-center.destroy', $notification->id))
        ->assertOk()->assertJsonPath('success', true);
});

Hapus personal berarti menyembunyikan dari tampilan user tersebut (via system_notification_user.deleted_at), bukan menghapus notifikasi dari database.

Feature Flag

Test Case Setting User Expected
Flag dinonaktifkan feature_notification_center = false Viewer biasa 403 Forbidden
Flag dinonaktifkan feature_notification_center = false User dengan manage global settings 200 OK (admin tetap bisa akses)

Keamanan XSS di API Recent

test('recent notifications content is escaped', function () {
    Notification::create([
        'title'   => '<script>alert(1)</script>',  // judul berbahaya
        'message' => '<b>bold</b>',
    ]);
    $response = $this->actingAs($user)->getJson('/notification-center/api/recent')->json();
    $titles = collect($response['notifications'])->pluck('title')->all();
    foreach ($titles as $t) {
        expect($t)->not->toContain('<script>'); // harus di-escape
    }
});

10.4 AiCircuitBreakerTest.php — AI Circuit Breaker

File: tests/Feature/System/AiCircuitBreakerTest.php

Menguji bahwa circuit breaker memblokir upaya AI self-healing yang berlebihan dalam satu siklus, mencegah loop healing yang tidak terkontrol.

Setup: Buat banyak AiHealingLog dalam waktu singkat
Action: Panggil mekanisme healing
Expected: Circuit breaker aktif dan memblokir request selanjutnya

10.5 ImpersonateTest.php — Impersonasi User

File: tests/Feature/ImpersonateTest.php
Endpoint: POST /impersonate/{id}, POST /impersonate/stop

Test Case Expected
Guest mulai impersonasi Redirect ke /login
User tanpa permission 403 Forbidden
Impersonasi diri sendiri 403 Forbidden
Impersonasi user tidak aktif 403 Forbidden
Impersonasi Developer (Super Admin) 403 Forbidden
Impersonasi berhasil Auth berpindah ke target, session simpan impersonator_id
Impersonasi saat sudah impersonasi Ditolak dengan error
Stop tanpa sesi impersonasi aktif 403 Forbidden
Stop impersonasi Auth kembali ke admin asli, impersonator_id dihapus

11. Feature Tests — Services

11.1 PasswordPolicyServiceTest.php — Layanan Password Policy

File: tests/Feature/Services/Auth/PasswordPolicyServiceTest.php
Target: app/Services/Auth/PasswordPolicyService.php

Helper test lokal: setSetting('key', value) untuk mengatur konfigurasi tanpa boilerplate.

isPasswordExpired()

Test Case Kondisi Expected
Expiry dinonaktifkan password_expiry_days = 0 false (tidak pernah expired)
Ada password_changed_at baru Changed 10 hari lalu, batas 30 hari false
Ada password_changed_at lama Changed 40 hari lalu, batas 30 hari true
Tidak ada password_changed_at Gunakan created_at sebagai fallback Cek berdasarkan created_at

checkHistory()

Test Case Kondisi Expected
History count = 0 Fitur dinonaktifkan Tidak ada exception
Reuse password di riwayat Password ada di 3 riwayat terbaru Exception dilempar
Password berbeda Password baru tidak ada di riwayat Tidak ada exception
Hanya cek N terbaru History count = 2, cek 3 entry Hanya 2 terbaru yang dicek

recordPasswordChange()

test('recordPasswordChange creates history row and stamps password_changed_at', function () {
    setSetting('password_history_count', 5); // harus > 0 untuk simpan history
    $user = User::factory()->create(['password_changed_at' => null]);
    PasswordPolicyService::recordPasswordChange($user, Hash::make('new-pass'));
    expect($user->fresh()->password_changed_at)->not->toBeNull();
    expect(PasswordHistory::where('user_id', $user->id)->count())->toBe(1);
});

Bug yang diperbaiki: Test awal menggunakan password_history_count = 0, tetapi service dengan benar tidak menyimpan riwayat ketika nilai ini 0. Diperbaiki menjadi = 5.

getRules()

Memverifikasi bahwa objek Password yang dikembalikan mengandung aturan minimum dan maksimum dari setting sistem.

11.2 SystemConfigServiceTest.php — Layanan Konfigurasi Sistem

File: tests/Feature/Services/SystemConfig/SystemConfigServiceTest.php

Kelompok Test Yang Diverifikasi
definitions() Mengembalikan array metadata setting lengkap
all() dan get() Nilai default dari definisi, nilai dari DB override
getPublicSettings() Hanya setting dengan is_public = true
grouped() Setting dikelompokkan berdasarkan group
update() Buat/timpa baris, tulis revision
Tracking revision Setiap update menyimpan old_value dan new_value
Serialisasi bool Boolean diserialisasi dengan benar
Cache clearing Cache dibersihkan setelah update
Request tracking IP address dan user agent disimpan di revision

11.3 BackupManagementServiceTest.php — Layanan Backup

File: tests/Feature/Services/System/BackupManagementServiceTest.php

Test Case Verifikasi
checkRequirements() Mengembalikan {mysqldump: bool, php: bool}
testConnection() pada local disk Berhasil tanpa exception
parseBytes() (private) Parsing string byte dengan unit
formatBytes() (private) Format angka ke string berunit

12. Feature Tests — Performa

12.1 NPlusOneTest.php — Pencegahan N+1 Query

File: tests/Feature/Performance/NPlusOneTest.php

N+1 query adalah masalah performa umum di ORM: satu query untuk mendapatkan daftar, lalu satu query per baris untuk mengambil relasi. Untuk 100 baris, ini menghasilkan 101 query.

Test Users Datatable

Setup baseline: 5 users dengan role masing-masing
Hitung query: X queries untuk /users?draw=1&length=10
Tambah 20 users lagi
Hitung query lagi: Y queries
Verifikasi: Y - X < 10 (jumlah query tidak melonjak drastis)

Test Roles Datatable

Buat 5 role, masing-masing dengan permission
Hitung query untuk /roles?draw=1&length=10
Verifikasi: total query < 20

Test Permissions Datatable

Buat 8 permission
Hitung query untuk /permissions?draw=1&length=10
Verifikasi: total query < 20

Ketiga test ini membuktikan bahwa eager loading (with(['roles', 'permissions', 'creator'])) sudah diterapkan dengan benar di DataTable query.


13. Temuan Keamanan dan Perbaikan

Berikut adalah temuan konkret yang diidentifikasi dan diperbaiki selama whitebox testing:

13.0 XSS di Modal Detail Notifikasi (KRITIS — ditemukan pasca-audit)

Aspek Detail
Lokasi resources/views/layouts/app.blade.php baris ~22392250
Masalah Event handler klik .notification-clickable membaca data-message via jQuery .data(), yang otomatis men-decode HTML entity. Pesan lalu diproses regex markdown-lite (**bold**<strong>) dan dimasukkan ke DOM via .html(message). Penyerang yang bisa memasukkan notifikasi (memiliki permission manage notification center) bisa menulis payload <script>alert(1)</script> yang dieksekusi di browser semua penerima saat mereka mengklik kartu notifikasi.
Risiko XSS stored — eksekusi script di browser user yang menerima notifikasi
Perbaikan Menambahkan fungsi escapeHtml() di JS yang mengonversi <, >, &, ", ' ke HTML entity sebelum regex markdown-lite dijalankan. Dengan ini <script> menjadi &lt;script&gt; dan ditampilkan sebagai teks.
Perbaikan tambahan Menghapus field message_html (raw, tidak di-escape) dari respons JSON controller — field ini tidak digunakan di frontend namun berbahaya jika ada kode lain yang mengonsumsinya.
Test NotificationCenterTest.php::recent notifications content is escaped — sudah lulus

13.1 Session Fixation di OAuth Callback (KRITIS)

Aspek Detail
Lokasi app/Http/Controllers/Auth/SocialAuthController.php
Masalah Auth::login() dipanggil setelah OAuth callback tanpa session()->regenerate()
Risiko Penyerang yang mengetahui session ID pre-login dapat membajak session pengguna setelah mereka login via Google/GitHub
Perbaikan Menambahkan session()->regenerate() setelah Auth::login() di method callback()
Test SessionFixationTest.php::oauth callback regenerates the session id after successful login

13.2 XSS di Template AI Self-Healing (TINGGI)

Aspek Detail
Lokasi resources/views/pages/system_settings/ai-self-healing.blade.php
Masalah Penggunaan {!! ... !!} (raw output) dengan data dari database yang tidak di-escape
Risiko Jika error message dari database mengandung script tag, akan dieksekusi di browser admin
Perbaikan Diganti dengan {{ }} (escaped) dan @if Blade directive

13.3 Kolom Tabel yang Salah di Test Prunable (BUG TEST)

Model Masalah Perbaikan
OtpCode Test menggunakan email tapi kolom adalah identifier Ganti ke identifier
MobileSyncLog Test menggunakan device_id, created_at, updated_at yang tidak ada Hapus kolom yang tidak ada
MobileErrorLog Test menggunakan level, created_at, updated_at yang tidak ada Gunakan hanya message dan occurred_at

13.4 Bug Path Prefix di Upload Test (BUG TEST)

Aspek Detail
Masalah ltrim('/storage/editor/file.jpg', '/storage/') menghapus karakter, bukan prefix, menghasilkan ditor/...
Perbaikan Ganti dengan substr($url, strlen('/storage/'))

13.5 Setting Test yang Salah di PasswordPolicyService (BUG TEST)

Aspek Detail
Masalah Test recordPasswordChange set password_history_count = 0 lalu expect ada 1 baris history
Penjelasan Service benar tidak menyimpan history ketika count = 0
Perbaikan Ubah setting ke 5 dalam test

14. Statistik dan Cakupan

14.1 Ringkasan Eksekusi

Tests:    371 passed (1182 assertions)
Duration: ~48 detik
Hasil:    0 GAGAL, 0 ERROR

14.2 Distribusi Test per Kategori

Kategori File Test (est.)
Unit Tests 5 ~35
Feature — Autentikasi 10 ~55
Feature — API 5 ~25
Feature — Middleware & Keamanan 6 ~35
Feature — Access Control 3 ~30
Feature — Model & Database 2 ~25
Feature — Fitur Sistem 6 ~50
Feature — Services 3 ~30
Feature — Performa 1 ~5
Feature — Lainnya 7 ~40
Total 49 ~371

14.3 Test yang Ditambahkan Selama Whitebox Testing

File Keterangan
tests/Feature/Auth/PasswordControllerTest.php 10 test baru untuk PasswordController::update()
tests/Feature/Auth/SessionFixationTest.php 6 test session fixation di semua alur auth
tests/Feature/Api/ApiAuthExtendedTest.php 14 test untuk endpoint API kompleks
tests/Feature/System/SessionManagerTest.php 8 test untuk SessionManagerController
tests/Feature/System/EditorUploadTest.php 13 test untuk EditorUploadController
tests/Feature/System/NotificationCenterTest.php 15 test untuk NotificationCenterController
tests/Feature/Models/PrunableModelsTest.php 17 test untuk 8 Prunable model

Total penambahan: 83 test baru

14.4 Cakupan Jalur Kode Kritis

Jalur Kode Tercakup
Semua endpoint yang memerlukan autentikasi Ya — uji guest + user
Semua permission gate Ya — uji dengan dan tanpa izin
Semua validasi input Ya — uji happy path + invalid input
Semua kondisi session regeneration Ya — 6 alur diuji
Semua prunable model Ya — 8 model, 2 kondisi per model
Semua security header Ya — 6 header diverifikasi
Rate limiting endpoint sensitif Ya — 5 endpoint + isolasi IP
Cascade delete behavior Ya — 9 skenario FK

15. Cara Menjalankan Test

15.1 Persyaratan

  • Docker + Docker Compose terinstall
  • Container project-laravel.test-1 berjalan (Laravel Sail)
  • Database test (PostgreSQL) sudah di-migrate

15.2 Jalankan Semua Test

# Jalankan semua test di dalam container Docker
docker exec project-laravel.test-1 php artisan test --no-coverage

# Dengan output verbose (tampilkan setiap test)
docker exec project-laravel.test-1 php artisan test --no-coverage -v

15.3 Jalankan Test Spesifik

# File tertentu
docker exec project-laravel.test-1 php artisan test tests/Feature/Auth/SessionFixationTest.php --no-coverage

# Direktori tertentu
docker exec project-laravel.test-1 php artisan test tests/Feature/Auth/ --no-coverage

# Berdasarkan nama test (grep)
docker exec project-laravel.test-1 php artisan test --filter "session fixation" --no-coverage

15.4 Jalankan dengan Coverage Report

# Membutuhkan Xdebug atau PCOV
docker exec project-laravel.test-1 php artisan test --coverage --min=80

15.5 Menjalankan Hanya Unit atau Feature Tests

# Hanya Unit tests
docker exec project-laravel.test-1 php artisan test --testsuite=Unit

# Hanya Feature tests
docker exec project-laravel.test-1 php artisan test --testsuite=Feature

15.6 Troubleshooting

Error: "redis: Name or service not known"

Pastikan force="true" ada di semua tag <env> di phpunit.xml. Tanpa ini, konfigurasi .env yang menggunakan Redis tidak tertimpa.

Test gagal karena data tidak bersih antar test

Pastikan RefreshDatabase aktif. Cek tests/Pest.php bahwa uses(TestCase::class, RefreshDatabase::class)->in('Feature') sudah ada.

Test session fixation selalu lulus karena ID tidak berubah

Driver session array membuat ID baru di setiap request HTTP. Jika test session fixation gagal, pastikan actingAs() digunakan sebelum mengambil session()->getId().

Test "terminate own session" menghasilkan ID berbeda

Gunakan direct controller invocation via app(ControllerClass::class) daripada HTTP request, agar session ID tidak berubah antar panggilan.


Dokumentasi ini dibuat berdasarkan hasil whitebox testing yang dilakukan pada 16 Mei 2026. Semua 371 test lulus tanpa kegagalan.