store/tests/Feature/Admin/Hr/EmployeeTest.php

652 lines
24 KiB
PHP

<?php
use App\Enums\Permission as PermissionEnum;
use App\Models\Employee;
use App\Models\User;
use App\Models\UserProfile;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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 = 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 = 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 = 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('-');
});
});