From 8f67a099366491ad3efa53da8832693ae03f7e8f Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 22 Jun 2026 18:43:25 +0700 Subject: [PATCH] refactor: add createDeveloperUser function and test to exclude developer role users from employee index display --- tests/Feature/Admin/Hr/EmployeeTest.php | 27 + .../Feature/Admin/Master/RawMaterialTest.php | 595 ++++++++++++++++++ 2 files changed, 622 insertions(+) create mode 100644 tests/Feature/Admin/Master/RawMaterialTest.php diff --git a/tests/Feature/Admin/Hr/EmployeeTest.php b/tests/Feature/Admin/Hr/EmployeeTest.php index 9ddc17e..51a0912 100644 --- a/tests/Feature/Admin/Hr/EmployeeTest.php +++ b/tests/Feature/Admin/Hr/EmployeeTest.php @@ -46,6 +46,20 @@ function createEmployeeWithProfile(): User 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 [ @@ -128,6 +142,19 @@ function validEmployeePayload(): array ->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 ─────────────────────────────────────────────── diff --git a/tests/Feature/Admin/Master/RawMaterialTest.php b/tests/Feature/Admin/Master/RawMaterialTest.php new file mode 100644 index 0000000..58597c9 --- /dev/null +++ b/tests/Feature/Admin/Master/RawMaterialTest.php @@ -0,0 +1,595 @@ +seed(RolePermissionSeeder::class); + Storage::fake('public'); +}); + +// ─── Helper ─────────────────────────────────────────────── + +function createRawMaterialUserWithPermission(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 createRawMaterialWithPrices(): RawMaterial +{ + $rawMaterial = RawMaterial::factory()->create(); + + $prices = RawMaterialPrice::factory()->count(2)->create(['raw_material_id' => $rawMaterial->id]); + + foreach ($prices as $price) { + $price->addMedia(UploadedFile::fake()->image("{$price->variant}.jpg", 100, 100)) + ->toMediaCollection('images'); + } + + return $rawMaterial; +} + +function priceWithImage(string $variant = 'Default', int $price = 50000, float $stock = 10): array +{ + return [ + 'variant' => $variant, + 'price' => $price, + 'stock' => $stock, + 'images' => [UploadedFile::fake()->image("{$variant}.jpg", 100, 100)], + ]; +} + +// ─── Index ──────────────────────────────────────────────── + +describe('Raw Material Index', function () { + test('authenticated user with permission can view raw material index', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + createRawMaterialWithPrices(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.index')) + ->assertOk(); + }); + + test('guest is redirected to login', function () { + $this->get(route('admin.master.raw_materials.index')) + ->assertRedirect(route('login')); + }); + + test('user without permission is forbidden', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.index')) + ->assertForbidden(); + }); + + test('index displays raw materials with prices', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + createRawMaterialWithPrices(); + createRawMaterialWithPrices(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.index')) + ->assertOk(); + }); + + test('index can search raw materials by name', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $rawMaterial = RawMaterial::factory()->create(['name' => 'Kain Batik']); + RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.index', ['search' => 'Kain'])) + ->assertOk(); + }); + + test('index can filter by active status', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + createRawMaterialWithPrices(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.index', ['is_active' => '1'])) + ->assertOk(); + }); +}); + +// ─── Create ─────────────────────────────────────────────── + +describe('Raw Material Create', function () { + test('authenticated user with permission can view create form', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.create')) + ->assertOk(); + }); + + test('guest is redirected to login', function () { + $this->get(route('admin.master.raw_materials.create')) + ->assertRedirect(route('login')); + }); + + test('user without create permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.create')) + ->assertForbidden(); + }); +}); + +// ─── Store ──────────────────────────────────────────────── + +describe('Raw Material Store', function () { + test('authenticated user with permission can create a raw material', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [ + priceWithImage('Merah', 75000, 50), + ], + ]) + ->assertRedirect(route('admin.master.raw_materials.index')); + + $this->assertDatabaseHas('raw_materials', [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + ]); + }); + + test('guest cannot create a raw material', function () { + $this->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage()], + ])->assertRedirect(route('login')); + }); + + test('user without create permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage()], + ]) + ->assertForbidden(); + }); + + test('name is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => '', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage()], + ]) + ->assertSessionHasErrors('name'); + }); + + test('name must not exceed 200 characters', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => str_repeat('a', 201), + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage()], + ]) + ->assertSessionHasErrors('name'); + }); + + test('unit is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => '', + 'prices' => [priceWithImage()], + ]) + ->assertSessionHasErrors('unit'); + }); + + test('unit must be a valid enum value', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => 'invalid_unit', + 'prices' => [priceWithImage()], + ]) + ->assertSessionHasErrors('unit'); + }); + + test('prices is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [], + ]) + ->assertSessionHasErrors('prices'); + }); + + test('price variant is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [['variant' => '', 'price' => 50000, 'stock' => 10, 'images' => [UploadedFile::fake()->image('test.jpg')]]], + ]) + ->assertSessionHasErrors('prices.0.variant'); + }); + + test('price amount is required and must be greater than 0', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage('Merah', 0, 10)], + ]) + ->assertSessionHasErrors('prices.0.price'); + }); + + test('stock is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => '', 'images' => [UploadedFile::fake()->image('test.jpg')]]], + ]) + ->assertSessionHasErrors('prices.0.stock'); + }); + + test('stock must be non-negative', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Sutra', + 'unit' => RawMaterialUnit::METER->value, + 'prices' => [priceWithImage('Merah', 50000, -5)], + ]) + ->assertSessionHasErrors('prices.0.stock'); + }); + + test('creating raw material also creates prices', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); + + $this->actingAs($user) + ->post(route('admin.master.raw_materials.store'), [ + 'name' => 'Kain Premium', + 'unit' => RawMaterialUnit::YARD->value, + 'prices' => [ + priceWithImage('Merah', 75000, 50), + priceWithImage('Biru', 80000, 30), + ], + ]); + + $rawMaterial = RawMaterial::where('name', 'Kain Premium')->first(); + expect($rawMaterial)->not->toBeNull(); + expect($rawMaterial->prices)->toHaveCount(2); + }); +}); + +// ─── Edit ───────────────────────────────────────────────── + +describe('Raw Material Edit', function () { + test('authenticated user with permission can view edit form', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); + + $rawMaterial = createRawMaterialWithPrices(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.edit', $rawMaterial)) + ->assertOk(); + }); + + test('guest is redirected to login', function () { + $rawMaterial = createRawMaterialWithPrices(); + + $this->get(route('admin.master.raw_materials.edit', $rawMaterial)) + ->assertRedirect(route('login')); + }); + + test('user without update permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $rawMaterial = createRawMaterialWithPrices(); + + $this->actingAs($user) + ->get(route('admin.master.raw_materials.edit', $rawMaterial)) + ->assertForbidden(); + }); +}); + +// ─── Update ─────────────────────────────────────────────── + +describe('Raw Material Update', function () { + test('authenticated user with permission can update a raw material', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); + + $rawMaterial = createRawMaterialWithPrices(); + $price = $rawMaterial->prices->first(); + + $this->actingAs($user) + ->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => 'Nama Diubah', + 'unit' => RawMaterialUnit::KILOGRAM->value, + 'prices' => [ + ['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25], + ], + ]) + ->assertRedirect(route('admin.master.raw_materials.index')); + + expect($rawMaterial->fresh()->name)->toBe('Nama Diubah'); + expect($price->fresh()->variant)->toBe('Putih'); + }); + + test('guest cannot update a raw material', function () { + $rawMaterial = createRawMaterialWithPrices(); + $price = $rawMaterial->prices->first(); + + $this->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => 'Nama Baru', + 'unit' => $rawMaterial->unit->value, + 'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]], + ])->assertRedirect(route('login')); + }); + + test('user without update permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $rawMaterial = createRawMaterialWithPrices(); + $price = $rawMaterial->prices->first(); + + $this->actingAs($user) + ->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => 'Nama Baru', + 'unit' => $rawMaterial->unit->value, + 'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]], + ]) + ->assertForbidden(); + }); + + test('name is required on update', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); + + $rawMaterial = createRawMaterialWithPrices(); + $price = $rawMaterial->prices->first(); + + $this->actingAs($user) + ->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => '', + 'unit' => $rawMaterial->unit->value, + 'prices' => [ + ['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock], + ], + ]) + ->assertSessionHasErrors('name'); + }); + + test('prices is required on update', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); + + $rawMaterial = createRawMaterialWithPrices(); + + $this->actingAs($user) + ->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => 'Nama Bahan', + 'unit' => $rawMaterial->unit->value, + 'prices' => [], + ]) + ->assertSessionHasErrors('prices'); + }); + + test('unsubmitted prices are removed on update', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE); + + $rawMaterial = createRawMaterialWithPrices(); + $priceToKeep = $rawMaterial->prices->first(); + + $this->actingAs($user) + ->put(route('admin.master.raw_materials.update', $rawMaterial), [ + 'name' => 'Updated Material', + 'unit' => $rawMaterial->unit->value, + 'prices' => [ + ['id' => $priceToKeep->id, 'variant' => 'Kept', 'price' => 60000, 'stock' => 15], + ], + ]); + + $rawMaterial->refresh(); + expect($rawMaterial->prices)->toHaveCount(1); + expect($rawMaterial->prices->first()->variant)->toBe('Kept'); + }); +}); + +// ─── Toggle Status ──────────────────────────────────────── + +describe('Raw Material Toggle Status', function () { + test('authenticated user with permission can toggle raw material status', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS); + + $rawMaterial = RawMaterial::factory()->create(); + + $this->actingAs($user) + ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ + 'is_active' => false, + ]) + ->assertRedirect(); + + expect($rawMaterial->fresh()->is_active)->toBeFalse(); + }); + + test('guest cannot toggle raw material status', function () { + $rawMaterial = RawMaterial::factory()->create(); + + $this->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ + 'is_active' => false, + ])->assertRedirect(route('login')); + }); + + test('user without toggle permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $rawMaterial = RawMaterial::factory()->create(); + + $this->actingAs($user) + ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ + 'is_active' => false, + ]) + ->assertForbidden(); + }); + + test('is_active field is required', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS); + + $rawMaterial = RawMaterial::factory()->create(); + + $this->actingAs($user) + ->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [ + 'is_active' => null, + ]) + ->assertSessionHasErrors('is_active'); + }); +}); + +// ─── Destroy ────────────────────────────────────────────── + +describe('Raw Material Destroy', function () { + test('authenticated user with permission can delete a raw material', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); + + $rawMaterial = createRawMaterialWithPrices(); + + $this->actingAs($user) + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertRedirect(route('admin.master.raw_materials.index')); + + $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); + }); + + test('guest cannot delete a raw material', function () { + $rawMaterial = createRawMaterialWithPrices(); + + $this->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertRedirect(route('login')); + + $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); + }); + + test('user without delete permission is forbidden', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW); + + $rawMaterial = createRawMaterialWithPrices(); + + $this->actingAs($user) + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertForbidden(); + + $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); + }); + + test('deleting raw material also soft deletes prices', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); + + $rawMaterial = createRawMaterialWithPrices(); + $priceIds = $rawMaterial->prices->pluck('id')->toArray(); + + $this->actingAs($user) + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)); + + $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); + + foreach ($priceIds as $priceId) { + $this->assertSoftDeleted('raw_material_prices', ['id' => $priceId]); + } + }); +}); + +// ─── Raw Material Model ─────────────────────────────────── + +describe('Raw Material Model', function () { + test('raw material uses soft deletes', function () { + $rawMaterial = RawMaterial::factory()->create(); + + $rawMaterial->delete(); + + expect($rawMaterial->trashed())->toBeTrue(); + }); + + test('raw material can have prices', function () { + $rawMaterial = RawMaterial::factory()->create(); + RawMaterialPrice::factory()->count(3)->create(['raw_material_id' => $rawMaterial->id]); + + expect($rawMaterial->fresh()->prices)->toHaveCount(3); + }); + + test('raw material has is_active cast to boolean', function () { + $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); + + expect($rawMaterial->is_active)->toBeTrue(); + + $rawMaterial->update(['is_active' => false]); + + expect($rawMaterial->fresh()->is_active)->toBeFalse(); + }); + + test('raw material has unit cast to enum', function () { + $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]); + + expect($rawMaterial->unit)->toBe(RawMaterialUnit::YARD); + }); + + test('raw material has unit label accessor', function () { + $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]); + + expect($rawMaterial->unit_label)->toBe('Meter'); + }); + + test('raw material has unit abbreviation accessor', function () { + $rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::KILOGRAM->value]); + + expect($rawMaterial->unit_abbreviation)->toBe('kg'); + }); +});