Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion catatan_rilis.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ Di rilis versi v2602.0.0 di versi ini terdapat modul komentar pada artikel dan p
10. [#1451](https://github.com/OpenSID/OpenDK/issues/1451) Audit dan Standardisasi Konfigurasi Pest v4 untuk Konsistensi Testing.
11. [#1432](https://github.com/OpenSID/OpenDK/issues/1432) Code Quality Issues.
12. [#43](https://github.com/OpenSID/wiki-keamanan/issues/43) update kerentanan package yg digunakan OpenDK.
13. [#1454](https://github.com/OpenSID/OpenDK/issues/1454) Pengujian Fitur Autentikasi Komprehensif.
13. [#1454](https://github.com/OpenSID/OpenDK/issues/1454) Pengujian Fitur Autentikasi Komprehensif.
14. [#1452](https://github.com/OpenSID/OpenDK/issues/1452) Refactor Base TestCase ke Arsitektur Berbasis Trait (Pest v4).
74 changes: 74 additions & 0 deletions docs/DOKUMENTASI_TRAIT_TESTING_PEST_V4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Panduan Penggunaan Trait Testing (Pest v4) - OpenDK

Dokumen ini menjelaskan arsitektur testing berbasis **trait** yang diimplementasikan untuk mendukung **Pest v4**. Pendekatan ini lebih mengutamakan *composition* daripada *inheritance* yang berlebihan pada `TestCase.php`.

---

## 🛠 Trait yang Tersedia

### 1. `Tests\Traits\WithDatabaseSetup`
Mengelola isolasi database antar test.
- **Mekanisme**: Menggunakan `Illuminate\Foundation\Testing\DatabaseTransactions`.
- **Keamanan**: Trait ini **SANGAT AMAN** karena tidak melakukan pembersihan data (`RefreshDatabase`), melainkan hanya membungkus setiap test dalam transaksi database yang akan di-*rollback* secara otomatis.

### 2. `Tests\Traits\WithUserAuthentication`
Menyediakan helper untuk proses autentikasi.
- **Method**:
- `$this->actingAsUser($user = null)`: Login sebagai user biasa. Mengambil user pertama dari DB atau membuat baru via factory jika tidak ada.
- `$this->actingAsAdmin($admin = null)`: Login sebagai administrator. (Saat ini mengikuti pola `actingAsUser`).

### 3. `Tests\Traits\WithSettingAplikasi`
Digunakan untuk manipulasi konfigurasi aplikasi saat runtime testing.
- **Method**:
- `$this->setDefaultApplicationConfig()`: Mematikan fitur `sinkronisasi_database_gabungan` agar test berjalan lebih cepat dan terisolasi.

---

## 🚀 Cara Penggunaan

Gunakan fungsi `uses()` di dalam file test Pest Anda untuk menyertakan trait yang dibutuhkan.

### Contoh Implementasi

```php
<?php

use Tests\Traits\WithDatabaseSetup;
use Tests\Traits\WithUserAuthentication;
use Tests\Traits\WithSettingAplikasi;

// Mendaftarkan trait
uses(
WithDatabaseSetup::class,
WithUserAuthentication::class,
WithSettingAplikasi::class
);

beforeEach(function () {
// Jalankan setup yang dibutuhkan
$this->actingAsAdmin();
$this->setDefaultApplicationConfig();
});

test('halaman dashboard admin dapat diakses', function () {
$response = $this->get('/admin/dashboard');

$response->assertStatus(200);
});

test('perubahan data dibungkus dalam transaksi', function () {
\App\Models\User::factory()->create(['name' => 'Test Persistence']);

$this->assertDatabaseHas('users', ['name' => 'Test Persistence']);
// Data ini akan hilang setelah test ini selesai karena DatabaseTransactions
});
```

---

## 💡 Prinsip Testing di OpenDK

1. **Minimal TestCase.php**: Jangan tambahkan logic bisnis atau auth global ke `tests/TestCase.php`. Biarkan file tersebut sesederhana mungkin.
2. **Composition over Inheritance**: Jika Anda membutuhkan logic setup yang baru dan reusable, buatlah trait baru di folder `tests/Traits/`.
3. **No RefreshDatabase**: Hindari penggunaan `RefreshDatabase` karena akan menghapus data di database lokal pengembang. Gunakan selalu `WithDatabaseSetup` (DatabaseTransactions).
4. **Test Independence**: Setiap test harus bisa berjalan secara mandiri tanpa bergantung pada urutan eksekusi test lain.
21 changes: 7 additions & 14 deletions tests/CrudTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@

namespace Tests;

use App\Http\Middleware\Authenticate;
use App\Http\Middleware\CompleteProfile;
use App\Http\Middleware\GlobalShareMiddleware;
use App\Models\SettingAplikasi;
use Spatie\Permission\Middleware\PermissionMiddleware;
use Spatie\Permission\Middleware\RoleMiddleware;
use Tests\Traits\WithDatabaseSetup;
use Tests\Traits\WithSettingAplikasi;
use Tests\Traits\WithUserAuthentication;

class CrudTestCase extends TestCase
{
use CreatesApplication;
use WithDatabaseSetup;
use WithUserAuthentication;
use WithSettingAplikasi;

/**
* Set up the test environment.
Expand All @@ -49,13 +48,7 @@ protected function setUp(): void
{
parent::setUp();

$this->withViewErrors([]);
$this->withoutMiddleware([Authenticate::class, RoleMiddleware::class, PermissionMiddleware::class, CompleteProfile::class, GlobalShareMiddleware::class]); // Disable middleware for this test
// disabled database gabungan for testing
SettingAplikasi::updateOrCreate(
['key' => 'sinkronisasi_database_gabungan'],
['value' => '0']
);
$this->setDefaultApplicationConfig();
}

// Additional methods for CRUD tests can be added here
Expand Down
7 changes: 4 additions & 3 deletions tests/Feature/Security/MassAssignmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
'nama' => 'Test User',
'status_dasar' => 1,
'id' => 999, // Should not be assignable
'created_at' => $createdAt
'created_at' => $createdAt,
'desa_id' => 1,
];

$penduduk = Penduduk::create($data);

expect($penduduk->id)->not->toBe(999);
expect($penduduk->created_at->format('Y'))->toBe($createdAt->format('Y'));
})->group('security', 'mass-assignment');
Expand Down
69 changes: 38 additions & 31 deletions tests/Feature/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use App\Models\User;
use App\Models\Pengurus;
use Spatie\Permission\Models\Role;
use Database\Seeders\RefAgamaTableSeeder;
use Database\Seeders\RefPendidikanTableSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Storage;
Expand All @@ -47,12 +49,16 @@ class UserControllerTest extends CrudTestCase
protected function setUp(): void
{
parent::setUp();

// Create test roles
$this->createTestRoles();

// Create test users
$this->createTestUsers();

// Seed required tables for Pengurus factory
$this->seed(RefAgamaTableSeeder::class);
$this->seed(RefPendidikanTableSeeder::class);
}

/**
Expand All @@ -63,7 +69,7 @@ protected function setUp(): void
public function test_index_displays_user_list_view()
{
$user = User::first();

$response = $this->actingAs($user)->get(route('setting.user.index'));

$response->assertStatus(200);
Expand All @@ -79,10 +85,10 @@ public function test_index_displays_user_list_view()
*/
public function test_create_displays_user_creation_form()
{
$user = User::whereHas('roles', function($query) {
$user = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$response = $this->actingAs($user)->get(route('setting.user.create'));

$response->assertStatus(200);
Expand All @@ -101,14 +107,14 @@ public function test_create_displays_user_creation_form()
public function test_store_creates_new_user_successfully()
{
Storage::fake('public');
$user = User::whereHas('roles', function($query) {

$user = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$pengurus = Pengurus::factory()->create();
$role = Role::where('name', 'administrator-website')->first();

$userData = [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
Expand All @@ -119,13 +125,14 @@ public function test_store_creates_new_user_successfully()
'pengurus_id' => $pengurus->id,
'role' => [$role->name],
'status' => 1,
'agama_id' => 1
];

$response = $this->actingAs($user)->post(route('setting.user.store'), $userData);

$response->assertRedirect(route('setting.user.index'));
$response->assertSessionHas('success');

$this->assertDatabaseHas('users', [
'email' => $userData['email'],
'name' => $userData['name'],
Expand All @@ -139,10 +146,10 @@ public function test_store_creates_new_user_successfully()
*/
public function test_store_fails_with_invalid_data()
{
$user = User::whereHas('roles', function($query) {
$user = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$invalidData = [
'name' => '',
'email' => 'invalid-email',
Expand All @@ -153,7 +160,7 @@ public function test_store_fails_with_invalid_data()
$response = $this->actingAs($user)->post(route('setting.user.store'), $invalidData);

$response->assertSessionHasErrors(['name', 'email', 'password', 'address']);
}
}

/**
* Test edit method displays edit form for super admin.
Expand All @@ -162,10 +169,10 @@ public function test_store_fails_with_invalid_data()
*/
public function test_edit_displays_form_for_super_admin()
{
$superAdmin = User::whereHas('roles', function($query) {
$superAdmin = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$targetUser = User::where('id', '!=', $superAdmin->id)->first();

$response = $this->actingAs($superAdmin)->get(route('setting.user.edit', $targetUser->id));
Expand Down Expand Up @@ -198,10 +205,10 @@ public function test_edit_displays_form_for_user_editing_own_profile()
*/
public function test_update_updates_user_successfully_for_super_admin()
{
$superAdmin = User::whereHas('roles', function($query) {
$superAdmin = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$targetUser = User::where('id', '!=', $superAdmin->id)->first();
$role = Role::where('name', 'administrator-website')->first();

Expand All @@ -217,7 +224,7 @@ public function test_update_updates_user_successfully_for_super_admin()

$response->assertRedirect(route('setting.user.index'));
$response->assertSessionHas('success');

$this->assertDatabaseHas('users', [
'id' => $targetUser->id,
'name' => 'Updated Name',
Expand All @@ -232,7 +239,7 @@ public function test_update_updates_user_successfully_for_super_admin()
*/
public function test_update_updates_profile_and_redirects_to_dashboard_for_regular_user()
{
$regularUser = User::whereHas('roles', function($query) {
$regularUser = User::whereHas('roles', function ($query) {
$query->where('name', '!=', 'super-admin');
})->first();

Expand All @@ -247,32 +254,32 @@ public function test_update_updates_profile_and_redirects_to_dashboard_for_regul

$response->assertRedirect(route('dashboard'));
$response->assertSessionHas('success');

$this->assertDatabaseHas('users', [
'id' => $regularUser->id,
'name' => 'Updated Profile Name',
]);
}

/**
* Test destroy method deactivates user successfully.
*
* @return void
*/
public function test_destroy_deactivates_user_successfully()
{
$user = User::whereHas('roles', function($query) {
$user = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$targetUser = User::where('id', '!=', $user->id)->first();
$targetUser->status = 1;
$targetUser->save();

$response = $this->actingAs($user)->post(route('setting.user.destroy', $targetUser->id));

$response->assertRedirect(route('setting.user.index'));
$response->assertRedirect(route('setting.user.index'));

$this->assertDatabaseHas('users', [
'id' => $targetUser->id,
'status' => 0,
Expand All @@ -286,18 +293,18 @@ public function test_destroy_deactivates_user_successfully()
*/
public function test_active_activates_user_successfully()
{
$user = User::whereHas('roles', function($query) {
$user = User::whereHas('roles', function ($query) {
$query->where('name', 'super-admin');
})->first();

$targetUser = User::where('id', '!=', $user->id)->first();
$targetUser->status = 0;
$targetUser->save();

$response = $this->actingAs($user)->post(route('setting.user.active', $targetUser->id));

$response->assertRedirect(route('setting.user.index'));
$response->assertRedirect(route('setting.user.index'));

$this->assertDatabaseHas('users', [
'id' => $targetUser->id,
'status' => 1,
Expand All @@ -312,7 +319,7 @@ public function test_active_activates_user_successfully()
public function test_getDataUser_returns_json_response()
{
$user = User::first();

$response = $this->actingAs($user)->get(route('setting.user.getdata'));

$response->assertStatus(200);
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ abstract class TestCase extends BaseTestCase
*/
protected function setUp(): void
{
parent::setUp();
parent::setUp();

// Authenticate a user for all tests to prevent 403 errors
// This is necessary for Laravel 11 where authorization is stricter
Expand Down
18 changes: 18 additions & 0 deletions tests/Traits/WithDatabaseSetup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Tests\Traits;

use Illuminate\Foundation\Testing\DatabaseTransactions;

trait WithDatabaseSetup
{
use DatabaseTransactions;

/**
* Set up database for testing.
*/
protected function setUpDatabase(): void
{
// Add any additional database setup logic here if needed
}
}
Loading