From 8358e205752a90658f351c4782dea5d5d63d4546 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 2 Jan 2026 17:03:01 +0700 Subject: [PATCH] test: Add feature tests for user creation, editing, and index page, and refactor user index Livewire component data fetching. --- app/Livewire/Studio/Master/User/Index.php | 49 +++----- .../Feature/Studio/Master/User/CreateTest.php | 109 ++++++++++++++++++ tests/Feature/Studio/Master/User/EditTest.php | 64 ++++++++++ .../Feature/Studio/Master/User/IndexTest.php | 98 ++++++++++++++++ 4 files changed, 284 insertions(+), 36 deletions(-) create mode 100644 tests/Feature/Studio/Master/User/CreateTest.php create mode 100644 tests/Feature/Studio/Master/User/EditTest.php create mode 100644 tests/Feature/Studio/Master/User/IndexTest.php diff --git a/app/Livewire/Studio/Master/User/Index.php b/app/Livewire/Studio/Master/User/Index.php index e78d4d3..c9d3d90 100644 --- a/app/Livewire/Studio/Master/User/Index.php +++ b/app/Livewire/Studio/Master/User/Index.php @@ -21,52 +21,16 @@ class Index extends Component { use WithAuthorization, WithConfirmation, WithRequestNotification, WithSubscribeNotification, WithToast; - public Collection $employees; - public string $search = ''; public array $status = []; - public function mount(): void - { - $this->loadEmployees(); - } - - protected function loadEmployees(): void - { - $this->employees = Employee::with(['user', 'user.referralCode', 'user.outlets']) - ->when(! empty($this->search), function ($query) { - return $query->where('full_name', 'like', '%'.$this->search.'%') - ->orWhere('code', 'like', '%'.$this->search.'%') - ->orWhereHas('user', fn ($q) => $q->where('email', 'like', '%'.$this->search.'%')) - ->orWhereHas('user', fn ($q) => $q->where('username', 'like', '%'.$this->search.'%')); - }) - ->when(! empty($this->status), fn ($query) => $query->whereHas('user', fn ($q) => $q->whereIn('status', $this->status))) - ->withoutDeveloper() - ->latest() - ->get(); - } - - public function updatedSearch(string $value): void - { - $this->search = $value; - - $this->loadEmployees(); - } - - public function updatedStatus(): void - { - $this->loadEmployees(); - } - public function delete(User $user): void { $this->canOrAbort('delete user'); $user->delete(); - $this->loadEmployees(); - $this->toast('Pegawai berhasil dihapus.'); Flux::modals()->close(); @@ -85,8 +49,21 @@ public function resetPassword(User $user): void public function render(): View { + $employees = Employee::with(['user', 'user.referralCode', 'user.outlets']) + ->when(! empty($this->search), function ($query) { + return $query->where('full_name', 'like', '%' . $this->search . '%') + ->orWhere('code', 'like', '%' . $this->search . '%') + ->orWhereHas('user', fn($q) => $q->where('email', 'like', '%' . $this->search . '%')) + ->orWhereHas('user', fn($q) => $q->where('username', 'like', '%' . $this->search . '%')); + }) + ->when(! empty($this->status), fn($query) => $query->whereHas('user', fn($q) => $q->whereIn('status', $this->status))) + ->withoutDeveloper() + ->latest() + ->get(); + return view('livewire.studio.master.user.index', [ 'pageTitle' => 'Pegawai', + 'employees' => $employees, ]); } } diff --git a/tests/Feature/Studio/Master/User/CreateTest.php b/tests/Feature/Studio/Master/User/CreateTest.php new file mode 100644 index 0000000..60e1f1e --- /dev/null +++ b/tests/Feature/Studio/Master/User/CreateTest.php @@ -0,0 +1,109 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'create user']); + $role->givePermissionTo('create user'); + $this->user->assignRole($role); +}); + +it('renders the create user page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.user.create')) + ->assertOk() + ->assertSeeLivewire(Create::class); +}); + +it('validates required fields', function () { + Livewire::actingAs($this->user) + ->test(Create::class) + ->call('save') + ->assertHasErrors([ + 'form.full_name' => 'required', + 'form.username' => 'required', + 'form.email' => 'required', + 'form.phone_number' => 'required', + 'form.base_salary' => 'required', + 'form.birthdate' => 'required', + 'form.hire_date' => 'required', + 'form.gender' => 'required', + 'form.outlet_ids' => 'required', + 'form.role_ids' => 'required', + ]); +}); + +it('validates username minimum length', function () { + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.username', 'abc') + ->call('save') + ->assertHasErrors(['form.username' => 'min']); +}); + +it('validates birthdate must be at least 18 years ago', function () { + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.birthdate', Carbon::now()->subYears(17)->format('Y-m-d')) + ->call('save') + ->assertHasErrors(['form.birthdate' => 'before']); +}); + +it('can store a new user/employee', function () { + $outlet = Outlet::factory()->create(); + $role = Role::firstOrCreate(['name' => 'Admin']); + + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.full_name', 'Pegawai Baru') + ->set('form.username', 'pegawaibaru') + ->set('form.email', 'pegawai@example.com') + ->set('form.phone_number', '0812 3456 7890') + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', '2000-01-01') + ->set('form.hire_date', '2024-01-01') + ->set('form.gender', Gender::MALE->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors() + ->assertRedirect(route('studio.master.user.index', ['notification' => 'Pegawai berhasil ditambahkan.'])); + + $this->assertDatabaseHas('users', [ + 'username' => 'pegawaibaru', + 'email' => 'pegawai@example.com', + ]); + + $user = User::where('username', 'pegawaibaru')->first(); + $this->assertDatabaseHas('employees', [ + 'user_id' => $user->id, + 'full_name' => 'Pegawai Baru', + 'base_salary' => 5000000, + ]); + + expect($user->outlets)->toHaveCount(1); + expect($user->hasRole('Admin'))->toBeTrue(); +}); + +it('cannot create user without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + Livewire::actingAs($this->user) + ->test(Create::class) + ->set('form.full_name', 'Unauthorized User') + ->call('save') + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/User/EditTest.php b/tests/Feature/Studio/Master/User/EditTest.php new file mode 100644 index 0000000..71947c2 --- /dev/null +++ b/tests/Feature/Studio/Master/User/EditTest.php @@ -0,0 +1,64 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'update user']); + $role->givePermissionTo('update user'); + $this->user->assignRole($role); + + $this->targetUser = User::factory()->create(); + $this->targetEmployee = Employee::factory()->for($this->targetUser)->create(); + $this->targetUser->outlets()->attach($this->outlet); + $this->targetUser->assignRole(Role::firstOrCreate(['name' => 'Admin'])); +}); + +it('renders the edit user page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.user.edit', $this->targetUser)) + ->assertOk() + ->assertSeeLivewire(Edit::class); +}); + +it('loads existing user data correctly', function () { + Livewire::actingAs($this->user) + ->test(Edit::class, ['user' => $this->targetUser]) + ->assertSet('form.full_name', $this->targetEmployee->full_name) + ->assertSet('form.username', $this->targetUser->username); +}); + +it('can update a user', function () { + Livewire::actingAs($this->user) + ->test(Edit::class, ['user' => $this->targetUser]) + ->set('form.full_name', 'Pegawai Diperbarui') + ->set('form.status', UserStatus::INACTIVE->value) + ->call('save') + ->assertHasNoErrors() + ->assertRedirect(route('studio.master.user.index', ['notification' => 'Pegawai berhasil diperbarui.'])); + + $this->targetUser->refresh(); + $this->targetEmployee->refresh(); + + expect($this->targetEmployee->full_name)->toBe('Pegawai Diperbarui'); + expect($this->targetUser->status)->toBe(UserStatus::INACTIVE); +}); + +it('cannot update user without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + Livewire::actingAs($this->user) + ->test(Edit::class, ['user' => $this->targetUser]) + ->set('form.full_name', 'Unauthorized Edit') + ->call('save') + ->assertForbidden(); +}); diff --git a/tests/Feature/Studio/Master/User/IndexTest.php b/tests/Feature/Studio/Master/User/IndexTest.php new file mode 100644 index 0000000..a16246d --- /dev/null +++ b/tests/Feature/Studio/Master/User/IndexTest.php @@ -0,0 +1,98 @@ +setupUser(); + + $role = Role::firstOrCreate(['name' => 'Owner']); + Permission::firstOrCreate(['name' => 'view user']); + Permission::firstOrCreate(['name' => 'delete user']); + Permission::firstOrCreate(['name' => 'reset password user']); + $role->givePermissionTo(['view user', 'delete user', 'reset password user']); + $this->user->assignRole($role); +}); + +it('renders the user index page correctly', function () { + $this->actingAs($this->user) + ->get(route('studio.master.user.index')) + ->assertOk() + ->assertSeeLivewire(Index::class); +}); + +it('displays list of employees correctly', function () { + $employees = collect(); + foreach (range(1, 3) as $i) { + $employees->push(Employee::factory()->create()); + } + + Livewire::actingAs($this->user) + ->test(Index::class) + ->assertViewHas('employees', function ($viewEmployees) use ($employees) { + return $viewEmployees->count() >= 3; + }) + ->assertSee($employees->first()->full_name) + ->assertSee($employees->last()->full_name); +}); + +it('can search employees by name', function () { + $targetEmployee = Employee::factory()->create(['full_name' => 'Pegawai Spesifik']); + $otherEmployee = Employee::factory()->create(['full_name' => 'Biasa Saja']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->set('search', 'Spesifik') + ->assertSee($targetEmployee->full_name) + ->assertDontSee($otherEmployee->full_name); +}); + +it('can filter employees by status', function () { + $activeUser = User::factory()->active()->create(); + $activeEmployee = Employee::factory()->for($activeUser)->create(['full_name' => 'Pegawai Aktif']); + + $inactiveUser = User::factory()->inactive()->create(); + $inactiveEmployee = Employee::factory()->for($inactiveUser)->create(['full_name' => 'Pegawai Nonaktif']); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->set('status', [\App\Enums\UserStatus::ACTIVE->value]) + ->assertSee($activeEmployee->full_name) + ->assertDontSee($inactiveEmployee->full_name); +}); + +it('can delete a user', function () { + $userToDelete = User::factory()->create(); + Employee::factory()->for($userToDelete)->create(); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('delete', $userToDelete->id); + + $this->assertSoftDeleted('users', ['id' => $userToDelete->id]); + $this->assertSoftDeleted('employees', ['user_id' => $userToDelete->id]); +}); + +it('can reset user password', function () { + $userToReset = User::factory()->create(['password' => Hash::make('old-password')]); + + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('resetPassword', $userToReset->id); + + $userToReset->refresh(); + expect(Hash::check(config('myconfig.password_default'), $userToReset->password))->toBeTrue(); +}); + +it('cannot access index page without permission', function () { + $this->user->roles()->detach(); + $this->user->permissions()->detach(); + + $this->actingAs($this->user) + ->get(route('studio.master.user.index')) + ->assertForbidden(); +});