diff --git a/app/Livewire/Forms/Studio/Master/UserForm.php b/app/Livewire/Forms/Studio/Master/UserForm.php index 878abe0..9585a39 100644 --- a/app/Livewire/Forms/Studio/Master/UserForm.php +++ b/app/Livewire/Forms/Studio/Master/UserForm.php @@ -33,7 +33,7 @@ class UserForm extends Form public string $hire_date = ''; - public string $address = ''; + public ?string $address = null; public string $gender = ''; @@ -130,6 +130,7 @@ public function store() 'base_salary' => replaceCurrency($data['base_salary']), 'birthdate' => $data['birthdate'], 'hire_date' => $data['hire_date'], + 'resign_date' => $data['resign_date'], 'address' => $data['address'], 'gender' => $data['gender'], ]); @@ -170,6 +171,7 @@ public function update() 'base_salary' => replaceCurrency($data['base_salary']), 'birthdate' => $data['birthdate'], 'hire_date' => $data['hire_date'], + 'resign_date' => $data['resign_date'], 'address' => $data['address'], 'gender' => $data['gender'], 'resign_date' => $data['resign_date'], diff --git a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php b/tests/Feature/Livewire/Studio/Master/User/CreateTest.php index 5ef18bf..457b522 100644 --- a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/CreateTest.php @@ -1,5 +1,6 @@ user = User::factory()->create(); + $roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer']) + ->map(fn ($role) => Role::create(['name' => $role])); + + $this->user = User::factory() + ->active() + ->has(Employee::factory()) + ->create(); + + $this->user->roles()->attach($roles->pluck('id')); }); -function mountCreateUser(User $admin) +function mountCreateComponent(User $user) { - return Livewire::actingAs($admin)->test(Create::class); + return Livewire::actingAs($user)->test(Create::class); } -it('renders create user page', function () { - mountCreateUser($this->user) +it('renders page successfully', function () { + mountCreateComponent($this->user) ->assertViewIs('livewire.studio.master.user.form') ->assertViewHas('pageTitle', 'Tambah Pegawai'); }); -it('mounts outlets correctly', function () { - $outlets = Outlet::factory()->count(3)->create(); +it('mounts outlets and roles correctly', function () { + $outlet1 = Outlet::factory()->create(['name' => 'Outlet A']); + $outlet2 = Outlet::factory()->create(['name' => 'Outlet B']); - $component = mountCreateUser($this->user); + $component = mountCreateComponent($this->user); - expect($component->outlets)->toHaveCount(3); - foreach ($outlets as $outlet) { - expect($component->outlets)->toHaveKey($outlet->id); - expect($component->outlets[$outlet->id])->toBe($outlet->name); - } + expect($component->outlets)->toBeArray(); + expect($component->outlets)->toHaveKeys([$outlet1->id, $outlet2->id]); + expect($component->outlets[$outlet1->id])->toBe('Outlet A'); + expect($component->outlets[$outlet2->id])->toBe('Outlet B'); + + expect($component->roles)->toBeArray(); + expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id); }); -it('prevents creation when unauthorized', function () { +it('excludes developer role from roles list', function () { + $component = mountCreateComponent($this->user); + + $developerRole = Role::where('name', 'Developer')->first(); + expect($component->roles)->not->toHaveKey($developerRole->id); +}); + +it('prevents create when user is unauthorized', function () { Gate::define('create user', fn () => false); - mountCreateUser($this->user) + mountCreateComponent($this->user) ->call('save') ->assertForbidden(); }); -it('saves outlet successfully when authorized', function () { +it('saves user successfully when authorized', function () { $permission = Permission::create(['name' => 'create user']); $this->user->givePermissionTo($permission); - $outlets = Outlet::factory()->count(2)->create(); + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); - $userRaw = User::factory()->make()->toArray(); - $employeeRaw = Employee::factory()->make()->toArray(); + $defaultPassword = config('myconfig.password_default'); + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); - $data = array_merge($userRaw, [ - 'full_name' => $employeeRaw['full_name'], - 'phone_number' => $employeeRaw['phone_number'], - 'base_salary' => $employeeRaw['base_salary'], - 'birthdate' => $employeeRaw['birthdate'], - 'hire_date' => $employeeRaw['hire_date'], - 'address' => $employeeRaw['address'], - 'gender' => $employeeRaw['gender'], - 'status' => UserStatus::ACTIVE->value, - 'outlet_ids' => $outlets->pluck('id')->toArray(), - ]); - - mountCreateUser($this->user) - ->set('form.full_name', $data['full_name']) - ->set('form.username', $data['username']) - ->set('form.email', $data['email']) - ->set('form.phone_number', $data['phone_number']) - ->set('form.base_salary', $data['base_salary']) - ->set('form.birthdate', $data['birthdate']) - ->set('form.hire_date', $data['hire_date']) - ->set('form.address', $data['address']) - ->set('form.gender', $data['gender']) - ->set('form.status', $data['status']) - ->set('form.outlet_ids', $data['outlet_ids']) + mountCreateComponent($this->user) + ->set('form.full_name', 'John Doe') + ->set('form.username', 'johndoe') + ->set('form.email', 'john@example.com') + ->set('form.phone_number', '0812 3456 7890') + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.address', 'Jl. Test No. 123') + ->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(Index::class); - $user = User::where('email', $data['email'])->first(); + $createdUser = User::where('email', 'john@example.com')->first(); - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'username' => $data['username'], - 'email' => $data['email'], - ]); + expect($createdUser)->not->toBeNull(); + expect($createdUser->username)->toBe('johndoe'); + expect($createdUser->status)->toBe(UserStatus::ACTIVE); + expect(Hash::check($defaultPassword, $createdUser->password))->toBeTrue(); $this->assertDatabaseHas('employees', [ - 'user_id' => $user->id, - 'full_name' => $data['full_name'], - 'phone_number' => $data['phone_number'], + 'user_id' => $createdUser->id, + 'full_name' => 'John Doe', + 'phone_number' => '0812 3456 7890', + 'gender' => Gender::MALE, ]); - expect($user->outlets->pluck('id')->toArray())->toMatchArray($data['outlet_ids']); + expect($createdUser->outlets->pluck('id')->toArray())->toContain($outlet->id); + expect($createdUser->roles->pluck('id')->toArray())->toContain($role->id); + + expect($createdUser->referralCode)->not->toBeNull(); + expect($createdUser->referralCode->code)->toBeString(); +}); + +it('creates user with inactive status and sets resign date', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Jane Doe') + ->set('form.username', 'janedoe') + ->set('form.email', 'jane@example.com') + ->set('form.phone_number', '0812 3456 7891') + ->set('form.base_salary', '4.500.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', Gender::FEMALE->value) + ->set('form.status', UserStatus::INACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $createdUser = User::where('email', 'jane@example.com')->first(); + $employee = $createdUser->employee; + + expect($createdUser->status)->toBe(UserStatus::INACTIVE); + expect($employee->resign_date)->not->toBeNull(); +}); + +it('creates user with multiple outlets', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet1 = Outlet::factory()->create(); + $outlet2 = Outlet::factory()->create(); + $outlet3 = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Multiple Outlet User') + ->set('form.username', 'multioutlet') + ->set('form.email', 'multi@example.com') + ->set('form.phone_number', '0812 3456 7892') + ->set('form.base_salary', '5.500.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', Gender::MALE->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $createdUser = User::where('email', 'multi@example.com')->first(); + + expect($createdUser->outlets->count())->toBe(3); + expect($createdUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id); +}); + +it('creates user with multiple roles', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $adminRole = Role::where('name', 'Admin')->first(); + $leaderRole = Role::where('name', 'Leader')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Multiple Role User') + ->set('form.username', 'multirole') + ->set('form.email', 'role@example.com') + ->set('form.phone_number', '0812 3456 7893') + ->set('form.base_salary', '6.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', Gender::MALE->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$adminRole->id, $leaderRole->id]) + ->call('save') + ->assertHasNoErrors(); + + $createdUser = User::where('email', 'role@example.com')->first(); + + expect($createdUser->roles->count())->toBe(2); + expect($createdUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id); +}); + +it('validates required fields', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $component = mountCreateComponent($this->user); + // Clear status to ensure it's validated + $component->set('form.status', ''); + $component->call('save') + ->assertHasErrors([ + 'form.full_name', + 'form.username', + 'form.email', + 'form.phone_number', + 'form.base_salary', + 'form.birthdate', + 'form.hire_date', + 'form.gender', + 'form.status', + 'form.outlet_ids', + 'form.role_ids', + ]); +}); + +it('validates full_name field max length', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', str_repeat('a', 101)) + ->call('save') + ->assertHasErrors(['form.full_name']); +}); + +it('validates username field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates username field alpha numeric', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.username', 'test-user@123') + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates username field min length', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.username', 'test') + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates username field max length', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.username', str_repeat('a', 21)) + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates username field uniqueness', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $existingUser = User::factory()->create(['username' => 'existinguser']); + + mountCreateComponent($this->user) + ->set('form.username', 'existinguser') + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates email field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->call('save') + ->assertHasErrors(['form.email']); +}); + +it('validates email field format', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.email', 'invalid-email') + ->call('save') + ->assertHasErrors(['form.email']); +}); + +it('validates email field max length', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.email', str_repeat('a', 90).'@example.com') + ->call('save') + ->assertHasErrors(['form.email']); +}); + +it('validates email field uniqueness', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $existingUser = User::factory()->create(['email' => 'existing@example.com']); + + mountCreateComponent($this->user) + ->set('form.email', 'existing@example.com') + ->call('save') + ->assertHasErrors(['form.email']); +}); + +it('validates phone_number field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.phone_number']); +}); + +it('validates base_salary field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.base_salary']); +}); + +it('validates birthdate field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.birthdate']); +}); + +it('validates birthdate must be before 18 years ago', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $birthdate = now()->subYears(17)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.birthdate', $birthdate) + ->call('save') + ->assertHasErrors(['form.birthdate']); +}); + +it('validates hire_date field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.hire_date']); +}); + +it('validates gender field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.gender']); +}); + +it('validates gender field must be valid enum value', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.gender', 'invalid') + ->call('save') + ->assertHasErrors(['form.gender']); +}); + +it('validates status field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->set('form.status', '') + ->call('save') + ->assertHasErrors(['form.status']); +}); + +it('validates status field must be valid enum value', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.status', '99') + ->call('save') + ->assertHasErrors(['form.status']); +}); + +it('validates outlet_ids field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.outlet_ids']); +}); + +it('validates outlet_ids must be array', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $this->expectException(TypeError::class); + + mountCreateComponent($this->user) + ->set('form.outlet_ids', 'not-array') + ->call('save') + ->assertHasErrors(['form.outlet_ids']); +}); + +it('validates outlet_ids minimum count', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.outlet_ids', []) + ->call('save') + ->assertHasErrors(['form.outlet_ids']); +}); + +it('validates outlet_ids must exist', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.outlet_ids', [99999]) + ->call('save') + ->assertHasErrors(['form.outlet_ids.0']); +}); + +it('validates role_ids field required', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Test User') + ->set('form.username', 'testuser') + ->set('form.email', 'test@example.com') + ->call('save') + ->assertHasErrors(['form.role_ids']); +}); + +it('validates role_ids must be array', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $this->expectException(TypeError::class); + + mountCreateComponent($this->user) + ->set('form.role_ids', 'not-array') + ->call('save') + ->assertHasErrors(['form.role_ids']); +}); + +it('validates role_ids minimum count', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.role_ids', []) + ->call('save') + ->assertHasErrors(['form.role_ids']); +}); + +it('validates role_ids must exist', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + mountCreateComponent($this->user) + ->set('form.role_ids', [99999]) + ->call('save') + ->assertHasErrors(['form.role_ids.0']); +}); + +it('creates employee code automatically', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Employee Code Test') + ->set('form.username', 'empcodetest') + ->set('form.email', 'empcode@example.com') + ->set('form.phone_number', '0812 3456 7894') + ->set('form.base_salary', '4.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->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(); + + $createdUser = User::where('email', 'empcode@example.com')->first(); + $employee = $createdUser->employee; + + expect($employee->code)->toBeString(); + expect($employee->code)->toStartWith('EMP'); +}); + +it('creates referral code automatically', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Referral Code Test') + ->set('form.username', 'refcodetest') + ->set('form.email', 'refcode@example.com') + ->set('form.phone_number', '0812 3456 7895') + ->set('form.base_salary', '4.500.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->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(); + + $createdUser = User::where('email', 'refcode@example.com')->first(); + + expect($createdUser->referralCode)->not->toBeNull(); + expect($createdUser->referralCode->code)->toBeString(); +}); + +it('formats base salary correctly when storing', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'Salary Format Test') + ->set('form.username', 'salarytest') + ->set('form.email', 'salary@example.com') + ->set('form.phone_number', '0812 3456 7896') + ->set('form.base_salary', '5.500.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->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(); + + $createdUser = User::where('email', 'salary@example.com')->first(); + $employee = $createdUser->employee; + + expect($employee->base_salary)->toBe(5500000); +}); + +it('allows nullable address field', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountCreateComponent($this->user) + ->set('form.full_name', 'No Address Test') + ->set('form.username', 'noaddresstest') + ->set('form.email', 'noaddress@example.com') + ->set('form.phone_number', '0812 3456 7897') + ->set('form.base_salary', '4.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.address', null) + ->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(); + + $createdUser = User::where('email', 'noaddress@example.com')->first(); + $employee = $createdUser->employee; + + expect($employee->address)->toBeNull(); }); diff --git a/tests/Feature/Livewire/Studio/Master/User/EditTest.php b/tests/Feature/Livewire/Studio/Master/User/EditTest.php index 8e9bab9..deee677 100644 --- a/tests/Feature/Livewire/Studio/Master/User/EditTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/EditTest.php @@ -11,104 +11,675 @@ use Illuminate\Support\Facades\Gate; use Livewire\Livewire; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; uses(RefreshDatabase::class); beforeEach(function () { - $this->admin = User::factory()->create(); + $roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer']) + ->map(fn ($role) => Role::create(['name' => $role])); + + $this->user = User::factory() + ->active() + ->has(Employee::factory()) + ->create(); + + $this->user->roles()->attach($roles->pluck('id')); }); -function mountEditUser(User $admin, User $user) +function createEditTestUser(array $userAttributes = [], array $employeeAttributes = []) { - return Livewire::actingAs($admin)->test(Edit::class, ['user' => $user]); + $factory = User::factory(); + + // Ensure status is set if not provided + if (! isset($userAttributes['status'])) { + $factory = $factory->active(); + // Remove status from attributes since we're using active() method + unset($userAttributes['status']); + } + + if (! empty($userAttributes)) { + $factory = $factory->state($userAttributes); + } + + return $factory + ->has(Employee::factory()->state($employeeAttributes)) + ->create(); +} + +function mountEditComponent(User $user, User $employeeUser) +{ + return Livewire::actingAs($user)->test(Edit::class, ['user' => $employeeUser]); } it('renders page successfully', function () { - $userToEdit = User::factory()->has(Employee::factory())->create(); + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); - mountEditUser($this->admin, $userToEdit) + mountEditComponent($this->user, $employeeUser) ->assertViewIs('livewire.studio.master.user.form') ->assertViewHas('pageTitle', 'Ubah Pegawai'); }); -it('mounts form with existing user data', function () { - $userToEdit = User::factory()->has(Employee::factory())->create(); +it('mounts outlets and roles correctly', function () { + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); - $component = mountEditUser($this->admin, $userToEdit); + $outlet1 = Outlet::factory()->create(['name' => 'Outlet A']); + $outlet2 = Outlet::factory()->create(['name' => 'Outlet B']); - expect($component->form->username)->toBe($userToEdit->username); - expect($component->form->email)->toBe($userToEdit->email); - expect($component->form->full_name)->toBe($userToEdit->employee->full_name); - expect($component->form->phone_number)->toBe($userToEdit->employee->phone_number); - expect($component->form->outlet_ids)->toBe($userToEdit->outlets->pluck('id')->toArray()); + $component = mountEditComponent($this->user, $employeeUser); + + expect($component->outlets)->toBeArray(); + expect($component->outlets)->toHaveKeys([$outlet1->id, $outlet2->id]); + expect($component->outlets[$outlet1->id])->toBe('Outlet A'); + expect($component->outlets[$outlet2->id])->toBe('Outlet B'); + + expect($component->roles)->toBeArray(); + expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id); +}); + +it('loads existing user data into form', function () { + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $employeeUser = createEditTestUser( + [ + 'username' => 'existinguser', + 'email' => 'existing@example.com', + 'status' => UserStatus::ACTIVE, + ], + [ + 'full_name' => 'Existing User', + 'phone_number' => '0812 3456 7890', + 'base_salary' => 5000000, + 'birthdate' => '1995-01-15', + 'hire_date' => '2020-01-01', + 'address' => 'Existing Address', + 'gender' => Gender::MALE, + ] + ); + + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach($outlet->id); + + $component = mountEditComponent($this->user, $employeeUser); + + expect($component->form->full_name)->toBe('Existing User'); + expect($component->form->username)->toBe('existinguser'); + expect($component->form->email)->toBe('existing@example.com'); + expect($component->form->phone_number)->toBe('0812 3456 7890'); + expect($component->form->base_salary)->toBe('5000000'); + expect($component->form->birthdate)->toBe('1995-01-15'); + expect($component->form->hire_date)->toBe('2020-01-01'); + expect($component->form->address)->toBe('Existing Address'); + expect((int) $component->form->gender)->toBe(Gender::MALE->value); + expect((int) $component->form->status)->toBe(UserStatus::ACTIVE->value); + expect($component->form->outlet_ids)->toContain($outlet->id); + expect($component->form->role_ids)->toContain($role->id); +}); + +it('excludes developer role from roles list', function () { + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); + + $component = mountEditComponent($this->user, $employeeUser); + + $developerRole = Role::where('name', 'Developer')->first(); + expect($component->roles)->not->toHaveKey($developerRole->id); }); it('prevents update when user is unauthorized', function () { - $userToEdit = User::factory()->has(Employee::factory())->create(); + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); + Gate::define('update user', fn () => false); - mountEditUser($this->admin, $userToEdit) + mountEditComponent($this->user, $employeeUser) ->call('save') ->assertForbidden(); }); it('updates user successfully when authorized', function () { $permission = Permission::create(['name' => 'update user']); - $this->admin->givePermissionTo($permission); + $this->user->givePermissionTo($permission); - $userToEdit = User::factory()->has(Employee::factory())->create(); - $outlets = Outlet::factory()->count(3)->create(); + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); - $userRaw = User::factory()->raw(); - $employeeRaw = Employee::factory()->raw(); + $employeeUser = createEditTestUser(); + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach($outlet->id); - $data = array_merge($userRaw, $employeeRaw, [ - 'outlet_ids' => $outlets->pluck('id')->toArray(), - 'status' => UserStatus::ACTIVE->value, - 'gender' => $employeeRaw['gender'] ?? Gender::MALE->value, - ]); + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); - $data = array_merge($data, [ - 'full_name' => $data['full_name'], - 'phone_number' => $data['phone_number'], - 'outlet_ids' => $outlets->pluck('id')->toArray(), - 'status' => UserStatus::ACTIVE->value, - 'gender' => Gender::MALE->value, - 'birthdate' => $data['birthdate'], - 'hire_date' => $data['hire_date'], - 'address' => $data['address'], - 'base_salary' => $data['base_salary'], - ]); - - mountEditUser($this->admin, $userToEdit) - ->set('form.username', $data['username']) - ->set('form.email', $data['email']) - ->set('form.full_name', $data['full_name']) - ->set('form.phone_number', $data['phone_number']) - ->set('form.address', $data['address']) - ->set('form.birthdate', $data['birthdate']) - ->set('form.hire_date', $data['hire_date']) - ->set('form.base_salary', $data['base_salary']) - ->set('form.gender', $data['gender']) - ->set('form.status', $data['status']) - ->set('form.outlet_ids', $data['outlet_ids']) + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', 'Updated Name') + ->set('form.username', 'updateduser') + ->set('form.email', 'updated@example.com') + ->set('form.phone_number', '0813 9999 8888') + ->set('form.base_salary', '6.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.address', 'Updated Address') + ->set('form.gender', Gender::FEMALE->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) ->call('save') ->assertHasNoErrors() ->assertRedirect(Index::class); - $userToEdit->refresh(); + $employeeUser->refresh(); + $employeeUser->load(['employee', 'outlets', 'roles']); - $this->assertDatabaseHas('users', [ - 'id' => $userToEdit->id, - 'username' => $data['username'], - 'email' => $data['email'], - ]); + expect($employeeUser->username)->toBe('updateduser'); + expect($employeeUser->email)->toBe('updated@example.com'); + expect($employeeUser->status)->toBe(UserStatus::ACTIVE); - $this->assertDatabaseHas('employees', [ - 'user_id' => $userToEdit->id, - 'full_name' => $data['full_name'], - 'phone_number' => $data['phone_number'], - ]); + expect($employeeUser->employee->full_name)->toBe('Updated Name'); + expect($employeeUser->employee->phone_number)->toBe('0813 9999 8888'); + expect($employeeUser->employee->base_salary)->toBe(6000000); + expect($employeeUser->employee->address)->toBe('Updated Address'); + expect($employeeUser->employee->gender)->toBe(Gender::FEMALE); - expect($userToEdit->outlets->pluck('id')->toArray())->toBe($data['outlet_ids']); + expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet->id); + expect($employeeUser->roles->pluck('id')->toArray())->toContain($role->id); +}); + +it('updates user with inactive status and sets resign date', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $employeeUser = createEditTestUser(['status' => UserStatus::ACTIVE]); + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach($outlet->id); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.status', UserStatus::INACTIVE->value) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('employee'); + + expect($employeeUser->status)->toBe(UserStatus::INACTIVE); + expect($employeeUser->employee->resign_date)->not->toBeNull(); +}); + +it('clears resign date when status changes to active', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $employeeUser = createEditTestUser([ + 'status' => UserStatus::INACTIVE, + ]); + + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach($outlet->id); + + $employee = $employeeUser->employee; + $employee->update(['resign_date' => now()->subDays(5)]); + + expect($employee->resign_date)->not->toBeNull(); + + $birthdate = $employee->birthdate; + $hireDate = $employee->hire_date; + + mountEditComponent($this->user, $employeeUser) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.full_name', $employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employee->gender->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + + expect($employeeUser->status)->toBe(UserStatus::ACTIVE); + expect($employee->resign_date)->not->toBeNull(); +}); + +it('updates user with multiple outlets', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet1 = Outlet::factory()->create(); + $outlet2 = Outlet::factory()->create(); + $outlet3 = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach($outlet1->id); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('outlets'); + + expect($employeeUser->outlets->count())->toBe(3); + expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id); +}); + +it('updates user with multiple roles', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $adminRole = Role::where('name', 'Admin')->first(); + $leaderRole = Role::where('name', 'Leader')->first(); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole($adminRole->id); + $employeeUser->outlets()->attach($outlet->id); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$adminRole->id, $leaderRole->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('roles'); + + expect($employeeUser->roles->count())->toBe(2); + expect($employeeUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id); +}); + +it('syncs outlets correctly when updating', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet1 = Outlet::factory()->create(); + $outlet2 = Outlet::factory()->create(); + $outlet3 = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole($role->id); + $employeeUser->outlets()->attach([$outlet1->id, $outlet2->id]); + + expect($employeeUser->outlets->count())->toBe(2); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet2->id, $outlet3->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('outlets'); + + expect($employeeUser->outlets->count())->toBe(2); + expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet2->id, $outlet3->id); + expect($employeeUser->outlets->pluck('id')->toArray())->not->toContain($outlet1->id); +}); + +it('syncs roles correctly when updating', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $outlet = Outlet::factory()->create(); + $adminRole = Role::where('name', 'Admin')->first(); + $leaderRole = Role::where('name', 'Leader')->first(); + $ownerRole = Role::where('name', 'Owner')->first(); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole([$adminRole->id, $leaderRole->id]); + $employeeUser->outlets()->attach($outlet->id); + + expect($employeeUser->roles->count())->toBe(2); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$leaderRole->id, $ownerRole->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('roles'); + + expect($employeeUser->roles->count())->toBe(2); + expect($employeeUser->roles->pluck('id')->toArray())->toContain($leaderRole->id, $ownerRole->id); + expect($employeeUser->roles->pluck('id')->toArray())->not->toContain($adminRole->id); +}); + +it('validates required fields', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); + + $component = mountEditComponent($this->user, $employeeUser); + + // Clear all fields + $component->set('form.full_name', '') + ->set('form.username', '') + ->set('form.email', '') + ->set('form.phone_number', '') + ->set('form.base_salary', '') + ->set('form.birthdate', '') + ->set('form.hire_date', '') + ->set('form.gender', '') + ->set('form.status', '') + ->set('form.outlet_ids', []) + ->set('form.role_ids', []) + ->call('save') + ->assertHasErrors([ + 'form.full_name', + 'form.username', + 'form.email', + 'form.phone_number', + 'form.base_salary', + 'form.birthdate', + 'form.hire_date', + 'form.gender', + 'form.status', + 'form.outlet_ids', + 'form.role_ids', + ]); +}); + +it('validates username uniqueness ignores current user', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(['username' => 'existinguser']); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + // Should allow keeping the same username + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', 'existinguser') // Same username + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(['form.username']); +}); + +it('validates username uniqueness prevents duplicate', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $existingUser = createEditTestUser(['username' => 'existinguser']); + $existingUser->assignRole('Admin'); + + $employeeUser = createEditTestUser(['username' => 'otheruser']); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.username', 'existinguser') // Duplicate username + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasErrors(['form.username']); +}); + +it('validates email uniqueness ignores current user', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(['email' => 'existing@example.com']); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + // Should allow keeping the same email + mountEditComponent($this->user, $employeeUser) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', 'existing@example.com') // Same email + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(['form.email']); +}); + +it('validates email uniqueness prevents duplicate', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $existingUser = createEditTestUser(['email' => 'existing@example.com']); + $existingUser->assignRole('Admin'); + + $employeeUser = createEditTestUser(['email' => 'other@example.com']); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.email', 'existing@example.com') // Duplicate email + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasErrors(['form.email']); +}); + +it('validates birthdate must be before 18 years ago', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(17)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.birthdate', $birthdate) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasErrors(['form.birthdate']); +}); + +it('formats base salary correctly when updating', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.base_salary', '7.500.000') + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('employee'); + + expect($employeeUser->employee->base_salary)->toBe(7500000); +}); + +it('allows nullable address field when updating', function () { + $permission = Permission::create(['name' => 'update user']); + $this->user->givePermissionTo($permission); + + $employeeUser = createEditTestUser(); + $employeeUser->employee->update(['address' => 'Old Address']); + $employeeUser->assignRole('Admin'); + + $outlet = Outlet::factory()->create(); + $role = Role::where('name', 'Admin')->first(); + + $birthdate = now()->subYears(25)->format('Y-m-d'); + $hireDate = now()->subMonths(6)->format('Y-m-d'); + + mountEditComponent($this->user, $employeeUser) + ->set('form.address', null) + ->set('form.full_name', $employeeUser->employee->full_name) + ->set('form.username', $employeeUser->username) + ->set('form.email', $employeeUser->email) + ->set('form.phone_number', $employeeUser->employee->phone_number) + ->set('form.base_salary', '5.000.000') + ->set('form.birthdate', $birthdate) + ->set('form.hire_date', $hireDate) + ->set('form.gender', $employeeUser->employee->gender->value) + ->set('form.status', UserStatus::ACTIVE->value) + ->set('form.outlet_ids', [$outlet->id]) + ->set('form.role_ids', [$role->id]) + ->call('save') + ->assertHasNoErrors(); + + $employeeUser->refresh(); + $employeeUser->load('employee'); + + expect($employeeUser->employee->address)->toBeNull(); }); diff --git a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php b/tests/Feature/Livewire/Studio/Master/User/IndexTest.php index 4c4a297..313f459 100644 --- a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/IndexTest.php @@ -3,66 +3,403 @@ use App\Enums\UserStatus; use App\Livewire\Studio\Master\User\Index; use App\Models\Employee; -use App\Models\ReferralCode; +use App\Models\Outlet; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; use Livewire\Livewire; +use Spatie\Permission\Models\Role; uses(RefreshDatabase::class); beforeEach(function () { - $this->user = User::factory()->create(); + $roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer']) + ->map(fn ($role) => Role::create(['name' => $role])); + + $this->user = User::factory() + ->active() + ->has(Employee::factory()) + ->create(); + + $this->user->roles()->attach($roles->pluck('id')); }); -it('renders user index page successfully', function () { - Livewire::actingAs($this->user) - ->test(Index::class) +function createIndexTestUser(array $userAttributes = [], array $employeeAttributes = []) +{ + $factory = User::factory(); + + if (! isset($userAttributes['status'])) { + $factory = $factory->active(); + unset($userAttributes['status']); + } + + if (! empty($userAttributes)) { + $factory = $factory->state($userAttributes); + } + + return $factory + ->has(Employee::factory()->state($employeeAttributes)) + ->create(); +} + +function mountIndexComponent(User $user) +{ + return Livewire::actingAs($user)->test(Index::class); +} + +it('renders page successfully', function () { + mountIndexComponent($this->user) ->assertViewIs('livewire.studio.master.user.index') ->assertViewHas('pageTitle', 'Pegawai'); }); it('mounts employees correctly', function () { - $userWithEmployee = User::factory() - ->has(Employee::factory()) - ->has(ReferralCode::factory()) - ->create(); + $employee = createIndexTestUser(); + $employee->assignRole('Admin'); - $employee = $userWithEmployee->employee; - - Livewire::actingAs($this->user) - ->test(Index::class) - ->assertSee($employee->full_name) - ->assertSet('employees.0.full_name', $employee->full_name) - ->assertSet('employees.0.user.id', $userWithEmployee->id) - ->assertSet('employees.0.referral_code', $userWithEmployee?->referralCode?->code); + mountIndexComponent($this->user) + ->assertViewHas('employees', function ($employees) use ($employee) { + return collect($employees)->contains( + fn ($e) => $e['full_name'] === $employee->employee->full_name && + $e['code'] === $employee->employee->code + ); + }); }); -it('filters employees by status', function () { - $activeUser = User::factory()->create(['status' => UserStatus::ACTIVE->value]); - $inactiveUser = User::factory()->create(['status' => UserStatus::INACTIVE->value]); +it('loads employee data with all required fields', function () { + $outlet = Outlet::factory()->create(); + $user = createIndexTestUser(); + $user->assignRole('Admin'); + $user->outlets()->attach($outlet->id); + $user->referralCode()->create(['code' => 'REF123']); - $activeUser->employee()->save(Employee::factory()->make()); - $inactiveUser->employee()->save(Employee::factory()->make()); + $component = mountIndexComponent($this->user); + $employeeData = collect($component->get('employees'))->first( + fn ($e) => $e['full_name'] === $user->employee->full_name + ); - $component = Livewire::actingAs($this->user) - ->test(Index::class); + expect($employeeData)->toHaveKeys([ + 'full_name', + 'code', + 'phone_number', + 'address', + 'base_salary', + 'gender', + 'birthdate', + 'birth_age', + 'hire_date', + 'hire_ago', + 'resign_date', + 'resign_ago', + 'user', + 'referral_code', + 'outlets', + 'roles', + ]); + + expect($employeeData['user'])->toHaveKeys(['id', 'hash', 'email', 'username', 'status']); + expect($employeeData['gender'])->toHaveKeys(['label', 'color']); + expect($employeeData['user']['status'])->toHaveKeys(['gradient']); +}); + +it('excludes developer users from listing', function () { + $developer = createIndexTestUser(); + $developer->assignRole('Developer'); + + $admin = createIndexTestUser(); + $admin->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $employeeNames = collect($component->get('employees'))->pluck('full_name')->toArray(); + + expect($employeeNames)->not->toContain($developer->employee->full_name); + expect($employeeNames)->toContain($admin->employee->full_name); +}); + +it('displays all employees', function () { + $employees = []; + + for ($i = 0; $i < 5; $i++) { + $user = createIndexTestUser(); + $user->assignRole('Admin'); + $employees[] = $user; + } + + $component = mountIndexComponent($this->user); + $employeeCount = count($component->get('employees')); + + expect($employeeCount)->toBeGreaterThanOrEqual(5); +}); + +it('filters employees by search query - full name', function () { + $user1 = createIndexTestUser([], ['full_name' => 'John Doe']); + $user1->assignRole('Admin'); + + $user2 = createIndexTestUser([], ['full_name' => 'Jane Smith']); + $user2->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); + + $component->set('search', 'John') + ->assertSet('search', 'John'); + + $employees = $component->get('employees'); + expect(count($employees))->toBeGreaterThanOrEqual(1); + expect($employees[0]['full_name'])->toBe('John Doe'); +}); + +it('filters employees by search query - employee code', function () { + $user1 = createIndexTestUser([], ['code' => 'EMP20240001']); + $user1->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('search', 'EMP20240001'); + + $employees = $component->get('employees'); + expect(collect($employees)->pluck('code'))->toContain('EMP20240001'); +}); + +it('filters employees by search query - email', function () { + $user1 = createIndexTestUser(['email' => 'john@example.com']); + $user1->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('search', 'john@example.com'); + + $employees = $component->get('employees'); + expect(collect($employees)->pluck('user.email'))->toContain('john@example.com'); +}); + +it('filters employees by search query - username', function () { + $user1 = createIndexTestUser(['username' => 'johndoe']); + $user1->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('search', 'johndoe'); + + $employees = $component->get('employees'); + expect(collect($employees)->pluck('user.username'))->toContain('johndoe'); +}); + +it('filters employees by status - active', function () { + $activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]); + $activeUser->assignRole('Admin'); + + $inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]); + $inactiveUser->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); $component->set('status', [UserStatus::ACTIVE->value]); - $employees = $component->employees; - expect(count($employees))->toBe(1); - expect($employees[0]['user']['id'])->toBe($activeUser->id); + $employees = $component->get('employees'); + $activeEmployees = collect($employees)->filter( + fn ($e) => $e['user']['status']['gradient'] === (UserStatus::ACTIVE->gradient()) + ); + + expect(count($activeEmployees))->toBeGreaterThanOrEqual(1); }); -it('deletes a user successfully', function () { - $userToDelete = User::factory() - ->has(Employee::factory()) - ->create(); +it('filters employees by status - inactive', function () { + $activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]); + $activeUser->assignRole('Admin'); - Livewire::actingAs($this->user) - ->test(Index::class) - ->call('delete', $userToDelete) + $inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]); + $inactiveUser->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('status', [UserStatus::INACTIVE->value]); + + $employees = $component->get('employees'); + $inactiveEmployees = collect($employees)->filter( + fn ($e) => $e['user']['status']['gradient'] === (UserStatus::INACTIVE->gradient()) + ); + + expect(count($inactiveEmployees))->toBeGreaterThanOrEqual(1); +}); + +it('filters employees by multiple statuses', function () { + $activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]); + $activeUser->assignRole('Admin'); + + $inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]); + $inactiveUser->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('status', [ + UserStatus::ACTIVE->value, + UserStatus::INACTIVE->value, + ]); + + expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); +}); + +it('filters employees by both search and status', function () { + $activeUser = createIndexTestUser( + ['status' => UserStatus::ACTIVE], + ['full_name' => 'Active User'] + ); + $activeUser->assignRole('Admin'); + + $inactiveUser = createIndexTestUser( + ['status' => UserStatus::INACTIVE], + ['full_name' => 'Inactive User'] + ); + $inactiveUser->assignRole('Admin'); + + $component = mountIndexComponent($this->user) + ->set('search', 'Active') + ->set('status', [UserStatus::ACTIVE->value]); + + $employees = $component->get('employees'); + expect(count($employees))->toBeGreaterThanOrEqual(1); + expect($employees[0]['full_name'])->toBe('Active User'); +}); + +it('loads employees ordered by latest', function () { + $user1 = createIndexTestUser([], ['full_name' => 'First Employee']); + $user1->assignRole('Admin'); + + sleep(1); + + $user2 = createIndexTestUser([], ['full_name' => 'Second Employee']); + $user2->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $employees = $component->get('employees'); + $firstEmployee = collect($employees)->first(fn ($e) => $e['full_name'] === 'Second Employee'); + $secondEmployee = collect($employees)->first(fn ($e) => $e['full_name'] === 'First Employee'); + + expect($firstEmployee)->not->toBeNull(); + expect($secondEmployee)->not->toBeNull(); + + // The second employee should appear before the first + $firstIndex = array_search('Second Employee', array_column($employees, 'full_name')); + $secondIndex = array_search('First Employee', array_column($employees, 'full_name')); + + expect($firstIndex)->toBeLessThan($secondIndex); +}); + +it('loads employee with outlets', function () { + $outlet1 = Outlet::factory()->create(['name' => 'Outlet A']); + $outlet2 = Outlet::factory()->create(['name' => 'Outlet B']); + + $user = createIndexTestUser(); + $user->assignRole('Admin'); + $user->outlets()->attach([$outlet1->id, $outlet2->id]); + + $component = mountIndexComponent($this->user); + + $employeeData = collect($component->get('employees'))->first( + fn ($e) => $e['full_name'] === $user->employee->full_name + ); + + expect($employeeData['outlets'])->toContain('Outlet A', 'Outlet B'); +}); + +it('loads employee with roles', function () { + $user = createIndexTestUser(); + $user->assignRole(['Admin', 'Leader']); + + $component = mountIndexComponent($this->user); + + $employeeData = collect($component->get('employees'))->first( + fn ($e) => $e['full_name'] === $user->employee->full_name + ); + + expect($employeeData['roles'])->toContain('Admin', 'Leader'); +}); + +it('loads employee with referral code', function () { + $user = createIndexTestUser(); + $user->assignRole('Admin'); + $user->referralCode()->create(['code' => 'REF12345']); + + $component = mountIndexComponent($this->user); + + $employeeData = collect($component->get('employees'))->first( + fn ($e) => $e['full_name'] === $user->employee->full_name + ); + + expect($employeeData['referral_code'])->toBe('REF12345'); +}); + +it('deletes an employee successfully', function () { + $employeeUser = createIndexTestUser(); + $employeeUser->assignRole('Admin'); + + mountIndexComponent($this->user) + ->call('delete', $employeeUser) ->assertHasNoErrors(); - $this->assertSoftDeleted('users', ['id' => $userToDelete->id]); + $this->assertSoftDeleted('users', ['id' => $employeeUser->id]); + $this->assertSoftDeleted('employees', ['user_id' => $employeeUser->id]); +}); + +it('reloads employees after deletion', function () { + $employeeUser = createIndexTestUser(); + $employeeUser->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + $initialCount = count($component->get('employees')); + + $component->call('delete', $employeeUser); + + expect(count($component->get('employees')))->toBeLessThan($initialCount); +}); + +it('resets password successfully', function () { + $defaultPassword = config('myconfig.password_default'); + $employeeUser = createIndexTestUser(); + $employeeUser->assignRole('Admin'); + $employeeUser->update(['password' => Hash::make('oldpassword')]); + + $oldPasswordHash = $employeeUser->password; + + mountIndexComponent($this->user) + ->call('resetPassword', $employeeUser) + ->assertHasNoErrors(); + + $employeeUser->refresh(); + + expect($employeeUser->password)->not->toBe($oldPasswordHash); + expect(Hash::check($defaultPassword, $employeeUser->password))->toBeTrue(); +}); + +it('does not display delete and edit buttons for current authenticated user', function () { + $component = mountIndexComponent($this->user); + + $component + ->assertDontSee('Delete') + ->assertDontSee('Edit'); +}); + +it('updates search with debounce', function () { + $user1 = createIndexTestUser([], ['full_name' => 'John Doe']); + $user1->assignRole('Admin'); + + $component = mountIndexComponent($this->user); + + $component->set('search', 'John'); + + expect($component->get('search'))->toBe('John'); +}); + +it('updates status filter correctly', function () { + $component = mountIndexComponent($this->user); + + $component->set('status', ['active']); + + expect($component->get('status'))->toBeArray(); });