seed(RolePermissionSeeder::class); $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); Storage::fake($disk); Storage::fake('public'); }); // ─── Helper ─────────────────────────────────────────────── function createEmployeeUserWithPermission(PermissionEnum ...$permissions): User { $user = User::factory()->create(); $user->givePermissionTo( array_merge( [PermissionEnum::DASHBOARD_VIEW->value], array_map(fn (PermissionEnum $p) => $p->value, $permissions) ) ); $user->forgetCachedPermissions(); return $user; } function createEmployeeWithProfile(): User { $user = User::factory()->create(); UserProfile::factory()->create([ 'user_id' => $user->id, 'full_name' => fake()->name(), 'phone_number' => fake()->numerify('08##########'), 'gender' => fake()->randomElement(['male', 'female']), ]); Employee::factory()->create(['user_id' => $user->id]); $user->assignRole('marketing-offline'); return $user; } function createDeveloperUser(): User { $user = User::factory()->create(); UserProfile::factory()->create([ 'user_id' => $user->id, 'full_name' => fake()->name(), 'phone_number' => fake()->numerify('08##########'), 'gender' => fake()->randomElement(['male', 'female']), ]); $user->assignRole('developer'); return $user; } function validEmployeePayload(): array { return [ 'email' => 'pegawai@example.com', 'username' => 'pegawai_baru', 'full_name' => 'Pegawai Baru', 'phone_number' => '081234567890', 'gender' => 'male', 'birth_date' => '1995-01-15', 'address' => 'Jl. Merdeka No. 1', 'role' => 'marketing-offline', 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]; } // ─── Index ──────────────────────────────────────────────── describe('Employee Index', function () { test('authenticated user with permission can view employee index', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $this->actingAs($user) ->get(route('admin.hr.employees.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.hr.employees.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.hr.employees.index')) ->assertForbidden(); }); test('index displays employees', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); createEmployeeWithProfile(); createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.index')) ->assertOk(); }); test('index can search by name', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $employee->profile->update(['full_name' => 'Budi Santoso']); $this->actingAs($user) ->get(route('admin.hr.employees.index', ['search' => 'Budi'])) ->assertOk(); }); test('index can filter by role', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.index', ['role' => 'marketing-offline'])) ->assertOk(); }); test('index can filter by is_active status', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.index', ['is_active' => '1'])) ->assertOk(); }); test('index does not display developer role users', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $developer = createDeveloperUser(); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.index')) ->assertOk() ->assertDontSee($developer->email) ->assertSee($employee->email); }); }); // ─── Create ─────────────────────────────────────────────── describe('Employee Create', function () { test('authenticated user with permission can view create form', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $this->actingAs($user) ->get(route('admin.hr.employees.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.hr.employees.create')) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $this->actingAs($user) ->get(route('admin.hr.employees.create')) ->assertForbidden(); }); }); // ─── Store ──────────────────────────────────────────────── describe('Employee Store', function () { test('authenticated user with permission can create an employee', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $this->actingAs($user) ->post(route('admin.hr.employees.store'), validEmployeePayload()) ->assertRedirect(route('admin.hr.employees.index')); $this->assertDatabaseHas('users', [ 'email' => 'pegawai@example.com', 'username' => 'pegawai_baru', ]); }); test('guest cannot create an employee', function () { $this->post(route('admin.hr.employees.store'), validEmployeePayload()) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $this->actingAs($user) ->post(route('admin.hr.employees.store'), validEmployeePayload()) ->assertForbidden(); }); test('email is required', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['email'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('email'); }); test('email must be unique', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $existing = User::factory()->create(); $payload = validEmployeePayload(); $payload['email'] = $existing->email; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('email'); }); test('username is required', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['username'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('username'); }); test('username must be unique', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $existing = User::factory()->create(); $payload = validEmployeePayload(); $payload['username'] = $existing->username; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('username'); }); test('full_name is required', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['full_name'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('full_name'); }); test('role is required', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['role'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('role'); }); test('role must be assignable', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['role'] = 'developer'; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('role'); }); test('join_date is required for non-owner roles', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['join_date'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('join_date'); }); test('employment_status is required for non-owner roles', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['employment_status'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('employment_status'); }); test('base_salary is required for non-owner roles', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['base_salary'] = ''; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('base_salary'); }); test('phone_number must be valid format', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $payload = validEmployeePayload(); $payload['phone_number'] = '123invalid'; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload) ->assertSessionHasErrors('phone_number'); }); test('creating employee also creates profile and employee record', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $this->actingAs($user) ->post(route('admin.hr.employees.store'), validEmployeePayload()); $newUser = User::where('email', 'pegawai@example.com')->first(); expect($newUser)->not->toBeNull(); expect($newUser->profile)->not->toBeNull(); expect($newUser->employee)->not->toBeNull(); expect($newUser->profile->full_name)->toBe('Pegawai Baru'); expect($newUser->employee->base_salary)->toBe(5000000); }); test('creating employee with profile photo s3 key links the photo', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_CREATE); $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $imageContent = \Illuminate\Http\UploadedFile::fake()->image('avatar.jpg')->get(); Storage::disk($disk)->put('fake-s3-key.jpg', $imageContent); $payload = validEmployeePayload(); $payload['profile_s3_key'] = 'fake-s3-key.jpg'; $this->actingAs($user) ->post(route('admin.hr.employees.store'), $payload); $newUser = User::where('email', 'pegawai@example.com')->first(); expect($newUser)->not->toBeNull(); expect($newUser->profile->getMedia('profile_photo')->count())->toBe(1); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Employee Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_UPDATE); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.edit', $employee)) ->assertOk(); }); test('guest is redirected to login', function () { $employee = createEmployeeWithProfile(); $this->get(route('admin.hr.employees.edit', $employee)) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->get(route('admin.hr.employees.edit', $employee)) ->assertForbidden(); }); }); // ─── Update ─────────────────────────────────────────────── describe('Employee Update', function () { test('authenticated user with permission can update an employee', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_UPDATE); $employee = createEmployeeWithProfile(); $payload = validEmployeePayload(); $payload['email'] = $employee->email; $payload['username'] = $employee->username; $payload['full_name'] = 'Nama Diubah'; $this->actingAs($user) ->put(route('admin.hr.employees.update', $employee), $payload) ->assertRedirect(route('admin.hr.employees.index')); expect($employee->fresh()->profile->full_name)->toBe('Nama Diubah'); }); test('guest cannot update an employee', function () { $employee = createEmployeeWithProfile(); $this->put(route('admin.hr.employees.update', $employee), validEmployeePayload()) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->put(route('admin.hr.employees.update', $employee), validEmployeePayload()) ->assertForbidden(); }); test('email must be unique on update (ignoring self)', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_UPDATE); $employee = createEmployeeWithProfile(); $payload = validEmployeePayload(); $payload['email'] = $employee->email; $payload['username'] = $employee->username; $this->actingAs($user) ->put(route('admin.hr.employees.update', $employee), $payload) ->assertRedirect(route('admin.hr.employees.index')); }); test('updating employee can upload new photo and delete existing photo', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_UPDATE); $employee = createEmployeeWithProfile(); $imageContent = \Illuminate\Http\UploadedFile::fake()->image('initial.jpg')->get(); Storage::disk('public')->put('initial.jpg', $imageContent); $employee->profile->addMediaFromDisk('initial.jpg', 'public')->toMediaCollection('profile_photo'); expect($employee->profile->fresh()->getMedia('profile_photo')->count())->toBe(1); $mediaId = $employee->profile->fresh()->getFirstMedia('profile_photo')->id; $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $imageContent2 = \Illuminate\Http\UploadedFile::fake()->image('updated.jpg')->get(); Storage::disk($disk)->put('updated.jpg', $imageContent2); $payload = validEmployeePayload(); $payload['email'] = $employee->email; $payload['username'] = $employee->username; $payload['profile_s3_key'] = 'updated.jpg'; $payload['remove_profile_photo_ids'] = [$mediaId]; $this->actingAs($user) ->put(route('admin.hr.employees.update', $employee), $payload); $freshProfile = $employee->fresh()->profile; expect($freshProfile->getMedia('profile_photo')->count())->toBe(1); expect($freshProfile->getFirstMedia('profile_photo')->id)->not->toBe($mediaId); }); }); // ─── Toggle Status ──────────────────────────────────────── describe('Employee Toggle Status', function () { test('authenticated user with permission can toggle status', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_TOGGLE_STATUS); $employee = createEmployeeWithProfile(); $originalStatus = $employee->is_active; $this->actingAs($user) ->patch(route('admin.hr.employees.toggle_status', $employee), [ 'is_active' => ! $originalStatus, ]) ->assertRedirect(route('admin.hr.employees.index')); expect($employee->fresh()->is_active)->toBe(! $originalStatus); }); test('guest cannot toggle status', function () { $employee = createEmployeeWithProfile(); $this->patch(route('admin.hr.employees.toggle_status', $employee), [ 'is_active' => false, ])->assertRedirect(route('login')); }); test('user without toggle permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->patch(route('admin.hr.employees.toggle_status', $employee), [ 'is_active' => false, ]) ->assertForbidden(); }); test('is_active field is required', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_TOGGLE_STATUS); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->patch(route('admin.hr.employees.toggle_status', $employee), [ 'is_active' => null, ]) ->assertSessionHasErrors('is_active'); }); }); // ─── Reset Password ─────────────────────────────────────── describe('Employee Reset Password', function () { test('authenticated user with permission can reset password', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_RESET_PASSWORD); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->post(route('admin.hr.employees.reset_password', $employee)) ->assertRedirect(route('admin.hr.employees.index')); }); test('guest cannot reset password', function () { $employee = createEmployeeWithProfile(); $this->post(route('admin.hr.employees.reset_password', $employee)) ->assertRedirect(route('login')); }); test('user without reset password permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->post(route('admin.hr.employees.reset_password', $employee)) ->assertForbidden(); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Employee Destroy', function () { test('authenticated user with permission can delete an employee', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_DELETE); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->delete(route('admin.hr.employees.destroy', $employee)) ->assertRedirect(route('admin.hr.employees.index')); $this->assertSoftDeleted('users', ['id' => $employee->id]); }); test('guest cannot delete an employee', function () { $employee = createEmployeeWithProfile(); $this->delete(route('admin.hr.employees.destroy', $employee)) ->assertRedirect(route('login')); $this->assertNotSoftDeleted('users', ['id' => $employee->id]); }); test('user without delete permission is forbidden', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW); $employee = createEmployeeWithProfile(); $this->actingAs($user) ->delete(route('admin.hr.employees.destroy', $employee)) ->assertForbidden(); $this->assertNotSoftDeleted('users', ['id' => $employee->id]); }); test('deleting employee also soft deletes profile and employee record', function () { $user = createEmployeeUserWithPermission(PermissionEnum::EMPLOYEES_VIEW, PermissionEnum::EMPLOYEES_DELETE); $employee = createEmployeeWithProfile(); $profileId = $employee->profile->id; $employeeId = $employee->employee->id; $this->actingAs($user) ->delete(route('admin.hr.employees.destroy', $employee)); $this->assertSoftDeleted('users', ['id' => $employee->id]); $this->assertSoftDeleted('user_profiles', ['id' => $profileId]); $this->assertSoftDeleted('employees', ['id' => $employeeId]); }); }); // ─── Employee Model ─────────────────────────────────────── describe('Employee Model Attributes', function () { test('join_date_formatted returns translated formatted date or fallback', function () { $employee = Employee::factory()->create(['join_date' => '2024-01-15']); expect($employee->join_date_formatted)->toBe('Senin, 15 Januari 2024'); $employeeNull = Employee::factory()->make(['join_date' => null]); expect($employeeNull->join_date_formatted)->toBe('-'); }); test('resign_date_formatted returns translated formatted date or null', function () { $employee = Employee::factory()->create(['resign_date' => '2024-06-20']); expect($employee->resign_date_formatted)->toBe('Kamis, 20 Juni 2024'); $employeeNull = Employee::factory()->make(['resign_date' => null]); expect($employeeNull->resign_date_formatted)->toBeNull(); }); test('join_date_input returns date formatted as Y-m-d', function () { $employee = Employee::factory()->create(['join_date' => '2024-01-15']); expect($employee->join_date_input)->toBe('2024-01-15'); }); test('base_salary_formatted returns formatted rupiah or fallback', function () { $employee = Employee::factory()->create(['base_salary' => 5000000]); expect($employee->base_salary_formatted)->toBe('Rp 5.000.000'); $employeeNull = Employee::factory()->make(['base_salary' => null]); expect($employeeNull->base_salary_formatted)->toBe('-'); }); });