56 KiB
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
- Pendahuluan
- Strategi dan Metodologi
- Infrastruktur dan Konfigurasi Test
- Unit Tests
- Feature Tests — Autentikasi
- Feature Tests — API
- Feature Tests — Middleware dan Keamanan
- Feature Tests — Access Control
- Feature Tests — Model dan Database
- Feature Tests — Fitur Sistem
- Feature Tests — Services
- Feature Tests — Performa
- Temuan Keamanan dan Perbaikan
- Statistik dan Cakupan
- 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
RefreshDatabasetrait - 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.envproduction yang menggunakan Redis. Tanpaforce="true", test akan mencoba konek ke Redis dan gagal karena hostnameredistidak 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 kembaliCheckLegalAgreement— 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.php — parseUserAgent()
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_enableddapat di-toggle - Tabel
webauthn_credentialsada 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
ThrottleRequestsdinonaktifkan secara global diPest.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 ketests/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 validUserTrustedDevice: 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:
- Hard delete tidak meninggalkan orphan records (data yatim)
- Audit columns di-nullify ketika actor dihapus (tidak ada FK broken)
- 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
arraymembuat ID baru di setiap HTTP request test. Memanggil controller langsung viaapp()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 karenaltrimmenghapus karakter satu per satu, bukan prefix string. Misalnya,ltrim('/storage/editor/file.jpg', '/storage/')menghasilkanditor/...karena karakter 'e' juga ada di daftar karakter yang dihapus. Diperbaiki menggunakansubstr($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 ~2239–2250 |
| 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 <script> 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-1berjalan (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>diphpunit.xml. Tanpa ini, konfigurasi.envyang menggunakan Redis tidak tertimpa.
Test gagal karena data tidak bersih antar test
Pastikan
RefreshDatabaseaktif. Cektests/Pest.phpbahwauses(TestCase::class, RefreshDatabase::class)->in('Feature')sudah ada.
Test session fixation selalu lulus karena ID tidak berubah
Driver session
arraymembuat ID baru di setiap request HTTP. Jika test session fixation gagal, pastikanactingAs()digunakan sebelum mengambilsession()->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.