diff --git a/tests/Feature/Admin/Master/CustomerTest.php b/tests/Feature/Admin/Master/CustomerTest.php index 3e04623..5455863 100644 --- a/tests/Feature/Admin/Master/CustomerTest.php +++ b/tests/Feature/Admin/Master/CustomerTest.php @@ -1,9 +1,19 @@ get(route('admin.master.customers.index')); $response->assertRedirect(route('login')); @@ -17,11 +27,17 @@ $response->assertOk(); }); +/* +|-------------------------------------------------------------------------- +| INDEX PAGE +|-------------------------------------------------------------------------- +*/ + test('customer index page displays customers', function () { $user = User::factory()->create(); $this->actingAs($user); - $customers = Customer::factory()->count(3)->create(); + Customer::factory()->count(3)->create(); $response = $this->get(route('admin.master.customers.index')); $response->assertOk(); @@ -31,6 +47,57 @@ ); }); +test('index page works with zero customers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 0) + ); +}); + +test('index page does not display soft-deleted customers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Customer::factory()->create(['name' => 'Active Customer']); + Customer::factory()->create(['name' => 'Deleted Customer'])->delete(); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 1) + ->where('customers.0.name', 'Active Customer') + ); +}); + +test('index page displays correct count after delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Customer::factory()->count(5)->create(); + $customer = Customer::first(); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 4) + ); +}); + +/* +|-------------------------------------------------------------------------- +| CREATE / STORE +|-------------------------------------------------------------------------- +*/ + test('customer can be created', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -80,6 +147,17 @@ $response->assertSessionHasErrors('name'); }); +test('customer name must be string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 12345, + ]); + + $response->assertSessionHasErrors('name'); +}); + test('customer name must not exceed 200 characters', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -91,6 +169,32 @@ $response->assertSessionHasErrors('name'); }); +test('customer name exactly 200 characters passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => str_repeat('a', 200), + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('customer name exactly 1 character passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'A', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'A', + ]); +}); + test('customer name must be unique', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -104,6 +208,224 @@ $response->assertSessionHasErrors('name'); }); +test('customer name uniqueness is case sensitive', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Customer::factory()->create(['name' => 'Customer ABC']); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer ABC', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('customer name with only whitespace is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('customer name with special characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer & Co. (PT)', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer & Co. (PT)', + ]); +}); + +test('customer name with HTML tags is accepted as plain text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => '', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => '', + ]); +}); + +test('customer name with numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer 123', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer 123', + ]); +}); + +test('customer name with only numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => '12345', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => '12345', + ]); +}); + +test('customer name with unicode characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Nono', + ]); +}); + +test('customer name with mixed case is stored as entered', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'customer tEsT', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'customer tEsT', + ]); +}); + +test('customer name with leading and trailing spaces is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => ' Customer Spasi ', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('customer name with parentheses and brackets is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer (A) [VIP]', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer (A) [VIP]', + ]); +}); + +test('customer name with multiple spaces is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer dengan spasi banyak', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer dengan spasi banyak', + ]); +}); + +test('customer can be created with name at exact DB column limit (200 chars)', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 200); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => $name, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => $name, + ]); +}); + +test('store flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Toast', + ]); + + $response->assertRedirect(); +}); + +test('store creates new customer in database', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), [ + 'name' => 'Database Check', + ]); + + $this->assertDatabaseCount('customers', 1); + $this->assertDatabaseHas('customers', [ + 'name' => 'Database Check', + ]); +}); + +test('multiple customers can be created sequentially', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Customer 1']); + $this->post(route('admin.master.customers.store'), ['name' => 'Customer 2']); + $this->post(route('admin.master.customers.store'), ['name' => 'Customer 3']); + + $this->assertDatabaseCount('customers', 3); + + $this->assertDatabaseHas('customers', ['name' => 'Customer 1']); + $this->assertDatabaseHas('customers', ['name' => 'Customer 2']); + $this->assertDatabaseHas('customers', ['name' => 'Customer 3']); +}); + +/* +|-------------------------------------------------------------------------- +| PHONE NUMBER VALIDATION +|-------------------------------------------------------------------------- +*/ + test('customer phone_number must be numeric', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -116,6 +438,339 @@ $response->assertSessionHasErrors('phone_number'); }); +test('customer phone_number accepts valid numeric string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Valid Phone', + 'phone_number' => '08123456789012345678', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Valid Phone', + 'phone_number' => '08123456789012345678', + ]); +}); + +test('customer phone_number must not exceed 20 digits', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Long Phone', + 'phone_number' => '081234567890123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number exactly 20 digits passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Max Phone', + 'phone_number' => '12345678901234567890', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('customer phone_number exactly 1 digit passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Min Phone', + 'phone_number' => '1', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Min Phone', + 'phone_number' => '1', + ]); +}); + +test('customer phone_number can be null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer No Phone', + 'phone_number' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer No Phone', + 'phone_number' => null, + ]); +}); + +test('customer phone_number with special characters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Special Phone', + 'phone_number' => '+62-812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with spaces is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Space Phone', + 'phone_number' => '0812 345 678', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with dashes is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Dash Phone', + 'phone_number' => '0812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with dots is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Dot Phone', + 'phone_number' => '0812.3456.7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with parentheses is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Paren Phone', + 'phone_number' => '(0812)3456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with plus sign is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Plus Phone', + 'phone_number' => '+6281234567890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with hash is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Hash Phone', + 'phone_number' => '#08123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with asterisk is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Star Phone', + 'phone_number' => '*08123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with letters and numbers is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Mixed Phone', + 'phone_number' => '0812abc3456', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with only letters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Letter Phone', + 'phone_number' => 'abcdefghij', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer phone_number with empty string is accepted as nullable', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Empty Phone', + 'phone_number' => '', + ]); + + $response->assertSessionHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| ADDRESS VALIDATION +|-------------------------------------------------------------------------- +*/ + +test('customer address can be null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer No Address', + 'address' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer No Address', + 'address' => null, + ]); +}); + +test('customer address accepts long text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $address = str_repeat('Jl. Panjang No. 123, RT 01/RW 02, Kelurahan Sukamaju, Kecamatan Menteng, Jakarta Pusat. ', 10); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Long Address', + 'address' => $address, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Long Address', + ]); +}); + +test('customer address with special characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Special Address', + 'address' => 'Jl. Raya No. 1/2, Lt. 3 (Gedung ABC)', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Special Address', + 'address' => 'Jl. Raya No. 1/2, Lt. 3 (Gedung ABC)', + ]); +}); + +test('customer address with unicode is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Unicode Address', + 'address' => 'Jl. Sudirman No. 123, Jakarta', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Unicode Address', + 'address' => 'Jl. Sudirman No. 123, Jakarta', + ]); +}); + +test('customer address with newlines is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Multiline Address', + 'address' => "Jl. Raya No. 1\nKota Bandung\nJawa Barat", + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Multiline Address', + ]); +}); + +test('customer address with HTML is accepted as plain text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer HTML Address', + 'address' => 'Jl. Raya No. 1', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer HTML Address', + 'address' => 'Jl. Raya No. 1', + ]); +}); + +test('customer address can be empty string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Empty Address', + 'address' => '', + ]); + + $response->assertSessionHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| UPDATE / PUT +|-------------------------------------------------------------------------- +*/ + test('customer can be updated', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -165,6 +820,202 @@ $response->assertSessionHasErrors('name'); }); +test('customer update name is required', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => '', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('customer update name must not exceed 200 characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => str_repeat('a', 201), + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('customer update phone_number must be numeric', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Updated Customer', + 'phone_number' => 'abc123', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer update phone_number with special characters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Updated Customer', + 'phone_number' => '+62-812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('customer can be updated with only name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['phone_number' => '08123456789', 'address' => 'Old Address']); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Updated Name Only', + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->name)->toBe('Updated Name Only'); + expect($customer->phone_number)->toBe('08123456789'); + expect($customer->address)->toBe('Old Address'); +}); + +test('customer can be updated multiple times', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['name' => 'Version 1']); + + $this->put(route('admin.master.customers.update', $customer), ['name' => 'Version 2']); + $customer->refresh(); + expect($customer->name)->toBe('Version 2'); + + $this->put(route('admin.master.customers.update', $customer), ['name' => 'Version 3']); + $customer->refresh(); + expect($customer->name)->toBe('Version 3'); +}); + +test('update flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Updated Name', + ]); + + $response->assertRedirect(); +}); + +test('customer update with special characters in name is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Customer & Co. (PT)', + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->name)->toBe('Customer & Co. (PT)'); +}); + +test('customer update with unicode characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['name' => 'Original']); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Customer Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->name)->toBe('Customer Nono'); +}); + +test('customer update with only whitespace name is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('customer update can clear phone_number to null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['phone_number' => '08123456789']); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => $customer->name, + 'phone_number' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->phone_number)->toBeNull(); +}); + +test('customer update can clear address to null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['address' => 'Old Address']); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => $customer->name, + 'address' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->address)->toBeNull(); +}); + +test('updating non-existent customer returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->put(route('admin.master.customers.update', 999999), [ + 'name' => 'Ghost Customer', + ]); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| DELETE / DESTROY +|-------------------------------------------------------------------------- +*/ + test('customer can be deleted', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -179,3 +1030,793 @@ $this->assertSoftDeleted('customers', ['id' => $customer->id]); }); + +test('customer is soft-deleted not permanently removed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + $this->assertDatabaseHas('customers', ['id' => $customer->id]); + $this->assertSoftDeleted('customers', ['id' => $customer->id]); +}); + +test('soft-deleted customer still exists in database', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + $this->assertDatabaseHas('customers', ['id' => $customer->id]); +}); + +test('soft-deleted customer has deleted_at timestamp', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + $customer->refresh(); + expect($customer->deleted_at)->not->toBeNull(); +}); + +test('deleted customer is excluded from getAll query', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Customer::factory()->create(['name' => 'Active']); + $deleted = Customer::factory()->create(['name' => 'Deleted']); + $deleted->delete(); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 1) + ->where('customers.0.name', 'Active') + ); +}); + +test('delete flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->delete(route('admin.master.customers.destroy', $customer)); + + $response->assertRedirect(); +}); + +test('multiple customers can be deleted one by one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customers = Customer::factory()->count(3)->create(); + + $this->delete(route('admin.master.customers.destroy', $customers[0])); + $this->delete(route('admin.master.customers.destroy', $customers[1])); + + $this->assertSoftDeleted('customers', ['id' => $customers[0]->id]); + $this->assertSoftDeleted('customers', ['id' => $customers[1]->id]); + $this->assertDatabaseHas('customers', ['id' => $customers[2]->id, 'deleted_at' => null]); +}); + +test('updating soft-deleted customer via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + $customer->delete(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Should Not Work', + ]); + + $response->assertStatus(404); +}); + +test('deleting soft-deleted customer via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + $customer->delete(); + + $response = $this->delete(route('admin.master.customers.destroy', $customer)); + + $response->assertStatus(404); +}); + +test('deleting non-existent customer returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->delete(route('admin.master.customers.destroy', 999999)); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| CUSTOMER - PURCHASE RELATIONSHIP +|-------------------------------------------------------------------------- +*/ + +test('customer with purchases can be deleted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + Purchase::factory()->count(3)->create(['customer_id' => $customer->id]); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + $this->assertSoftDeleted('customers', ['id' => $customer->id]); + // Purchases still exist because soft delete doesn't trigger cascade + $this->assertDatabaseCount('purchases', 3); +}); + +test('index page works correctly with customers that have purchases', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + Purchase::factory()->count(5)->create(['customer_id' => $customer->id]); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 1) + ); +}); + +/* +|-------------------------------------------------------------------------- +| AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS +|-------------------------------------------------------------------------- +*/ + +test('guest cannot create customer', function () { + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Unauthorized', + ]); + + $response->assertRedirect(route('login')); + $this->assertDatabaseMissing('customers', ['name' => 'Unauthorized']); +}); + +test('guest cannot update customer', function () { + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Unauthorized Update', + ]); + + $response->assertRedirect(route('login')); +}); + +test('guest cannot delete customer', function () { + $customer = Customer::factory()->create(); + + $response = $this->delete(route('admin.master.customers.destroy', $customer)); + + $response->assertRedirect(route('login')); + $this->assertDatabaseHas('customers', ['id' => $customer->id, 'deleted_at' => null]); +}); + +/* +|-------------------------------------------------------------------------- +| DATA INTEGRITY +|-------------------------------------------------------------------------- +*/ + +test('created customer has correct timestamps', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), [ + 'name' => 'Timestamp Test', + ]); + + $customer = Customer::where('name', 'Timestamp Test')->first(); + expect($customer->created_at)->not->toBeNull(); + expect($customer->updated_at)->not->toBeNull(); +}); + +test('updated customer has updated timestamp changed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + $originalUpdatedAt = $customer->updated_at; + + sleep(1); + + $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Timestamp Changed', + ]); + + $customer->refresh(); + expect($customer->updated_at->gt($originalUpdatedAt))->toBeTrue(); +}); + +test('customer ID is auto-incremented', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'First']); + $first = Customer::where('name', 'First')->first(); + + $this->post(route('admin.master.customers.store'), ['name' => 'Second']); + $second = Customer::where('name', 'Second')->first(); + + expect($second->id)->toBe($first->id + 1); +}); + +test('customer factory creates valid customer', function () { + $customer = Customer::factory()->create(); + + expect($customer->name)->not->toBeEmpty(); + expect($customer->id)->toBeInt(); +}); + +test('customer factory creates unique names', function () { + $customers = Customer::factory()->count(20)->create(); + + $names = $customers->pluck('name')->toArray(); + expect($names)->toHaveCount(count(array_unique($names))); +}); + +/* +|-------------------------------------------------------------------------- +| SOFT DELETE & RESTORE +|-------------------------------------------------------------------------- +*/ + +test('customer can be restored after soft delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['name' => 'Restored Customer']); + $customer->delete(); + + $this->assertSoftDeleted('customers', ['id' => $customer->id]); + + $customer->restore(); + + $this->assertDatabaseHas('customers', ['id' => $customer->id, 'deleted_at' => null]); +}); + +test('restored customer appears in index again', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['name' => 'Restored Customer']); + $customer->delete(); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 0) + ); + + $customer->restore(); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 1) + ->where('customers.0.name', 'Restored Customer') + ); +}); + +test('restored customer can be updated', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(['name' => 'Before Restore']); + $customer->delete(); + $customer->restore(); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'After Restore', + ]); + + $response->assertSessionHasNoErrors(); + + $customer->refresh(); + expect($customer->name)->toBe('After Restore'); +}); + +/* +|-------------------------------------------------------------------------- +| REALISTIC USER SCENARIOS +|-------------------------------------------------------------------------- +*/ + +test('user creates customer then immediately edits it', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Draft Customer']); + $customer = Customer::where('name', 'Draft Customer')->first(); + + $this->put(route('admin.master.customers.update', $customer), ['name' => 'Final Customer']); + + $customer->refresh(); + expect($customer->name)->toBe('Final Customer'); +}); + +test('user creates multiple customers and deletes one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Keep']); + $this->post(route('admin.master.customers.store'), ['name' => 'Delete Me']); + $this->post(route('admin.master.customers.store'), ['name' => 'Also Keep']); + + $toDelete = Customer::where('name', 'Delete Me')->first(); + $this->delete(route('admin.master.customers.destroy', $toDelete)); + + $response = $this->get(route('admin.master.customers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/customer/index') + ->has('customers', 2) + ); +}); + +test('user renames customer to match another existing customer name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Customer::factory()->create(['name' => 'Target Name']); + $customer = Customer::factory()->create(['name' => 'Source Name']); + + $response = $this->put(route('admin.master.customers.update', $customer), [ + 'name' => 'Target Name', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('user creates customer with Indonesian characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'PT Maju Jaya Sentosa', + 'phone_number' => '0215551234', + 'address' => 'Jl. Sudirman No. 123, Jakarta Selatan', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'PT Maju Jaya Sentosa', + 'phone_number' => '0215551234', + 'address' => 'Jl. Sudirman No. 123, Jakarta Selatan', + ]); +}); + +test('user tries to create customer without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user tries to update customer without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $customer = Customer::factory()->create(); + + $response = $this->put(route('admin.master.customers.update', $customer), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user rapidly submits same customer creation twice', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Rapid Submit']); + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Rapid Submit']); + + $response->assertSessionHasErrors('name'); + $this->assertDatabaseCount('customers', 1); +}); + +test('user creates customer with name containing chinese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => '电子产品供应商']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing japanese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'サプライヤー']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing korean characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => '공급업체']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing arabic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'المورد']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing cyrillic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Поставщик']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing thai characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'ซัพพลายเออร์']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing hindi characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'आपूर्तिकर्ता']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing greek characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Προμηθευτής']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing vietnamese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Nhà cung cấp']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing mixed unicode scripts', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Customer 电子产品']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing emoji', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => '🏢 Customer']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with very short name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'X']); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'X', + ]); +}); + +test('user creates customer with name containing mixed case and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'PT123ABC456']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'PT123ABC456', + ]); +}); + +test('user creates customer with name containing only spaces and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => ' 123 ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with very long name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 200); + + $response = $this->post(route('admin.master.customers.store'), ['name' => $name]); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => $name, + ]); +}); + +test('user creates customer then creates another with similar name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Customer ABC']); + $this->post(route('admin.master.customers.store'), ['name' => 'Customer XYZ']); + + $this->assertDatabaseCount('customers', 2); + $this->assertDatabaseHas('customers', ['name' => 'Customer ABC']); + $this->assertDatabaseHas('customers', ['name' => 'Customer XYZ']); +}); + +test('user creates customer with name containing curly braces', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => '{code}']); + + $this->assertDatabaseHas('customers', [ + 'name' => '{code}', + ]); +}); + +test('user creates customer with name containing angle brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => '']); + + $this->assertDatabaseHas('customers', [ + 'name' => '', + ]); +}); + +test('user creates customer with name containing semicolon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'A;B']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'A;B', + ]); +}); + +test('user creates customer with name containing colon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Time: Now']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Time: Now', + ]); +}); + +test('user creates customer with name containing backtick', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => '`Code`']); + + $this->assertDatabaseHas('customers', [ + 'name' => '`Code`', + ]); +}); + +test('user creates customer with name containing tilde', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Caf~']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Caf~', + ]); +}); + +test('user creates customer with name containing caret', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'A^B']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'A^B', + ]); +}); + +test('user creates customer with name containing percentage', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => '100% Natural']); + + $this->assertDatabaseHas('customers', [ + 'name' => '100% Natural', + ]); +}); + +test('user creates customer with name containing fullwidth characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => "ABCDE"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing fullwidth brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => "()【】"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing hiragana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'あいうえお']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing katakana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'アイウエオ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing hangul', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => '가나다라마']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing hebrew', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'אבגדהו']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing bengali', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'অআইঈউ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing tamil', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => 'அஆஇஈஉ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates customer with name containing email-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'user@example.com']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'user@example.com', + ]); +}); + +test('user creates customer with name containing URL-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'https://example.com']); + + $this->assertDatabaseHas('customers', [ + 'name' => 'https://example.com', + ]); +}); + +test('user creates customer with address containing Indonesian address format', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer Alamat', + 'address' => "Jl. Raya Bogor Km. 30\nRT 01/02\nKel. Sukamaju\nKec. Menteng\nJakarta Pusat 10310", + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer Alamat', + ]); +}); + +test('user creates customer with phone number in various Indonesian formats', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $phones = [ + '081234567890', + '6281234567890', + '0215551234', + '02155512345', + ]; + + foreach ($phones as $phone) { + $response = $this->post(route('admin.master.customers.store'), [ + 'name' => 'Customer ' . $phone, + 'phone_number' => $phone, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('customers', [ + 'name' => 'Customer ' . $phone, + 'phone_number' => $phone, + ]); + } +}); + +test('user creates customer then deletes it and creates new one with same name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.customers.store'), ['name' => 'Reusable']); + $customer = Customer::where('name', 'Reusable')->first(); + + $this->delete(route('admin.master.customers.destroy', $customer)); + + // Soft-deleted name still triggers unique validation + $response = $this->post(route('admin.master.customers.store'), ['name' => 'Reusable']); + $response->assertSessionHasErrors('name'); +}); + +test('user creates customer with name containing regex special characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.customers.store'), ['name' => '.*+?^${}()|[]\\']); + $response->assertSessionHasNoErrors(); +}); diff --git a/tests/Feature/Admin/Master/SupplierTest.php b/tests/Feature/Admin/Master/SupplierTest.php index c93d000..3353fbf 100644 --- a/tests/Feature/Admin/Master/SupplierTest.php +++ b/tests/Feature/Admin/Master/SupplierTest.php @@ -1,9 +1,19 @@ get(route('admin.master.suppliers.index')); $response->assertRedirect(route('login')); @@ -17,11 +27,17 @@ $response->assertOk(); }); +/* +|-------------------------------------------------------------------------- +| INDEX PAGE +|-------------------------------------------------------------------------- +*/ + test('supplier index page displays suppliers', function () { $user = User::factory()->create(); $this->actingAs($user); - $suppliers = Supplier::factory()->count(3)->create(); + Supplier::factory()->count(3)->create(); $response = $this->get(route('admin.master.suppliers.index')); $response->assertOk(); @@ -31,6 +47,57 @@ ); }); +test('index page works with zero suppliers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 0) + ); +}); + +test('index page does not display soft-deleted suppliers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Supplier::factory()->create(['name' => 'Active Supplier']); + Supplier::factory()->create(['name' => 'Deleted Supplier'])->delete(); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 1) + ->where('suppliers.0.name', 'Active Supplier') + ); +}); + +test('index page displays correct count after delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Supplier::factory()->count(5)->create(); + $supplier = Supplier::first(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 4) + ); +}); + +/* +|-------------------------------------------------------------------------- +| CREATE / STORE +|-------------------------------------------------------------------------- +*/ + test('supplier can be created', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -69,18 +136,6 @@ ]); }); -test('supplier phone_number must be numeric', function () { - $user = User::factory()->create(); - $this->actingAs($user); - - $response = $this->post(route('admin.master.suppliers.store'), [ - 'name' => 'Supplier With Letters', - 'phone_number' => 'abc123', - ]); - - $response->assertSessionHasErrors('phone_number'); -}); - test('supplier name is required', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -92,6 +147,17 @@ $response->assertSessionHasErrors('name'); }); +test('supplier name must be string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 12345, + ]); + + $response->assertSessionHasErrors('name'); +}); + test('supplier name must not exceed 200 characters', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -103,6 +169,32 @@ $response->assertSessionHasErrors('name'); }); +test('supplier name exactly 200 characters passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => str_repeat('a', 200), + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('supplier name exactly 1 character passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'A', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'A', + ]); +}); + test('supplier name must be unique', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -116,6 +208,569 @@ $response->assertSessionHasErrors('name'); }); +test('supplier name uniqueness is case sensitive', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Supplier::factory()->create(['name' => 'Supplier ABC']); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier ABC', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('supplier name with only whitespace is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('supplier name with special characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier & Co. (PT)', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier & Co. (PT)', + ]); +}); + +test('supplier name with HTML tags is accepted as plain text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => '', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '', + ]); +}); + +test('supplier name with numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier 123', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier 123', + ]); +}); + +test('supplier name with only numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => '12345', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '12345', + ]); +}); + +test('supplier name with unicode characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Nono', + ]); +}); + +test('supplier name with mixed case is stored as entered', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'sUpPlIeR tEsT', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'sUpPlIeR tEsT', + ]); +}); + +test('supplier name with leading and trailing spaces is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => ' Supplier Spasi ', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('supplier name with parentheses and brackets is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier (A) [VIP]', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier (A) [VIP]', + ]); +}); + +test('supplier name with multiple spaces is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier dengan spasi banyak', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier dengan spasi banyak', + ]); +}); + +test('supplier can be created with name at exact DB column limit (200 chars)', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 200); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => $name, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => $name, + ]); +}); + +test('store flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Toast', + ]); + + $response->assertRedirect(); +}); + +test('store creates new supplier in database', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Database Check', + ]); + + $this->assertDatabaseCount('suppliers', 1); + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Database Check', + ]); +}); + +test('multiple suppliers can be created sequentially', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier 1']); + $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier 2']); + $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier 3']); + + $this->assertDatabaseCount('suppliers', 3); + + $this->assertDatabaseHas('suppliers', ['name' => 'Supplier 1']); + $this->assertDatabaseHas('suppliers', ['name' => 'Supplier 2']); + $this->assertDatabaseHas('suppliers', ['name' => 'Supplier 3']); +}); + +/* +|-------------------------------------------------------------------------- +| PHONE NUMBER VALIDATION +|-------------------------------------------------------------------------- +*/ + +test('supplier phone_number must be numeric', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier With Letters', + 'phone_number' => 'abc123', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number accepts valid numeric string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Valid Phone', + 'phone_number' => '08123456789012345678', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Valid Phone', + 'phone_number' => '08123456789012345678', + ]); +}); + +test('supplier phone_number must not exceed 20 digits', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Long Phone', + 'phone_number' => '081234567890123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number exactly 20 digits passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Max Phone', + 'phone_number' => '12345678901234567890', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('supplier phone_number exactly 1 digit passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Min Phone', + 'phone_number' => '1', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Min Phone', + 'phone_number' => '1', + ]); +}); + +test('supplier phone_number can be null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier No Phone', + 'phone_number' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier No Phone', + 'phone_number' => null, + ]); +}); + +test('supplier phone_number with special characters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Special Phone', + 'phone_number' => '+62-812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with spaces is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Space Phone', + 'phone_number' => '0812 345 678', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with dashes is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Dash Phone', + 'phone_number' => '0812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with dots is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Dot Phone', + 'phone_number' => '0812.3456.7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with parentheses is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Paren Phone', + 'phone_number' => '(0812)3456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with plus sign is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Plus Phone', + 'phone_number' => '+6281234567890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with hash is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Hash Phone', + 'phone_number' => '#08123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with asterisk is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Star Phone', + 'phone_number' => '*08123456789', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with letters and numbers is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Mixed Phone', + 'phone_number' => '0812abc3456', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with only letters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Letter Phone', + 'phone_number' => 'abcdefghij', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier phone_number with empty string is accepted as nullable', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Empty Phone', + 'phone_number' => '', + ]); + + $response->assertSessionHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| ADDRESS VALIDATION +|-------------------------------------------------------------------------- +*/ + +test('supplier address can be null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier No Address', + 'address' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier No Address', + 'address' => null, + ]); +}); + +test('supplier address accepts long text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $address = str_repeat('Jl. Panjang No. 123, RT 01/RW 02, Kelurahan Sukamaju, Kecamatan Menteng, Jakarta Pusat. ', 10); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Long Address', + 'address' => $address, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Long Address', + ]); +}); + +test('supplier address with special characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Special Address', + 'address' => 'Jl. Raya No. 1/2, Lt. 3 (Gedung ABC)', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Special Address', + 'address' => 'Jl. Raya No. 1/2, Lt. 3 (Gedung ABC)', + ]); +}); + +test('supplier address with unicode is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Unicode Address', + 'address' => 'Jl. Sudirman No. 123, Jakarta', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Unicode Address', + 'address' => 'Jl. Sudirman No. 123, Jakarta', + ]); +}); + +test('supplier address with newlines is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Multiline Address', + 'address' => "Jl. Raya No. 1\nKota Bandung\nJawa Barat", + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Multiline Address', + ]); +}); + +test('supplier address with HTML is accepted as plain text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier HTML Address', + 'address' => 'Jl. Raya No. 1', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier HTML Address', + 'address' => 'Jl. Raya No. 1', + ]); +}); + +test('supplier address can be empty string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Empty Address', + 'address' => '', + ]); + + $response->assertSessionHasNoErrors(); +}); + +/* +|-------------------------------------------------------------------------- +| UPDATE / PUT +|-------------------------------------------------------------------------- +*/ + test('supplier can be updated', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -165,6 +820,202 @@ $response->assertSessionHasErrors('name'); }); +test('supplier update name is required', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => '', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('supplier update name must not exceed 200 characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => str_repeat('a', 201), + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('supplier update phone_number must be numeric', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Updated Supplier', + 'phone_number' => 'abc123', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier update phone_number with special characters is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Updated Supplier', + 'phone_number' => '+62-812-3456-7890', + ]); + + $response->assertSessionHasErrors('phone_number'); +}); + +test('supplier can be updated with only name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['phone_number' => '08123456789', 'address' => 'Old Address']); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Updated Name Only', + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->name)->toBe('Updated Name Only'); + expect($supplier->phone_number)->toBe('08123456789'); + expect($supplier->address)->toBe('Old Address'); +}); + +test('supplier can be updated multiple times', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['name' => 'Version 1']); + + $this->put(route('admin.master.suppliers.update', $supplier), ['name' => 'Version 2']); + $supplier->refresh(); + expect($supplier->name)->toBe('Version 2'); + + $this->put(route('admin.master.suppliers.update', $supplier), ['name' => 'Version 3']); + $supplier->refresh(); + expect($supplier->name)->toBe('Version 3'); +}); + +test('update flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Updated Name', + ]); + + $response->assertRedirect(); +}); + +test('supplier update with special characters in name is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Supplier & Co. (PT)', + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->name)->toBe('Supplier & Co. (PT)'); +}); + +test('supplier update with unicode characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['name' => 'Original']); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Supplier Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->name)->toBe('Supplier Nono'); +}); + +test('supplier update with only whitespace name is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('supplier update can clear phone_number to null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['phone_number' => '08123456789']); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => $supplier->name, + 'phone_number' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->phone_number)->toBeNull(); +}); + +test('supplier update can clear address to null', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['address' => 'Old Address']); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => $supplier->name, + 'address' => null, + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->address)->toBeNull(); +}); + +test('updating non-existent supplier returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->put(route('admin.master.suppliers.update', 999999), [ + 'name' => 'Ghost Supplier', + ]); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| DELETE / DESTROY +|-------------------------------------------------------------------------- +*/ + test('supplier can be deleted', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -179,3 +1030,793 @@ $this->assertSoftDeleted('suppliers', ['id' => $supplier->id]); }); + +test('supplier is soft-deleted not permanently removed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $this->assertDatabaseHas('suppliers', ['id' => $supplier->id]); + $this->assertSoftDeleted('suppliers', ['id' => $supplier->id]); +}); + +test('soft-deleted supplier still exists in database', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $this->assertDatabaseHas('suppliers', ['id' => $supplier->id]); +}); + +test('soft-deleted supplier has deleted_at timestamp', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $supplier->refresh(); + expect($supplier->deleted_at)->not->toBeNull(); +}); + +test('deleted supplier is excluded from getAll query', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Supplier::factory()->create(['name' => 'Active']); + $deleted = Supplier::factory()->create(['name' => 'Deleted']); + $deleted->delete(); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 1) + ->where('suppliers.0.name', 'Active') + ); +}); + +test('delete flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $response->assertRedirect(); +}); + +test('multiple suppliers can be deleted one by one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $suppliers = Supplier::factory()->count(3)->create(); + + $this->delete(route('admin.master.suppliers.destroy', $suppliers[0])); + $this->delete(route('admin.master.suppliers.destroy', $suppliers[1])); + + $this->assertSoftDeleted('suppliers', ['id' => $suppliers[0]->id]); + $this->assertSoftDeleted('suppliers', ['id' => $suppliers[1]->id]); + $this->assertDatabaseHas('suppliers', ['id' => $suppliers[2]->id, 'deleted_at' => null]); +}); + +test('updating soft-deleted supplier via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + $supplier->delete(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Should Not Work', + ]); + + $response->assertStatus(404); +}); + +test('deleting soft-deleted supplier via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + $supplier->delete(); + + $response = $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $response->assertStatus(404); +}); + +test('deleting non-existent supplier returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->delete(route('admin.master.suppliers.destroy', 999999)); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| SUPPLIER - PURCHASE RELATIONSHIP +|-------------------------------------------------------------------------- +*/ + +test('supplier with purchases can be deleted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + Purchase::factory()->count(3)->create(['supplier_id' => $supplier->id]); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $this->assertSoftDeleted('suppliers', ['id' => $supplier->id]); + // Purchases still exist because soft delete doesn't trigger cascade + $this->assertDatabaseCount('purchases', 3); +}); + +test('index page works correctly with suppliers that have purchases', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + Purchase::factory()->count(5)->create(['supplier_id' => $supplier->id]); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 1) + ); +}); + +/* +|-------------------------------------------------------------------------- +| AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS +|-------------------------------------------------------------------------- +*/ + +test('guest cannot create supplier', function () { + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Unauthorized', + ]); + + $response->assertRedirect(route('login')); + $this->assertDatabaseMissing('suppliers', ['name' => 'Unauthorized']); +}); + +test('guest cannot update supplier', function () { + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Unauthorized Update', + ]); + + $response->assertRedirect(route('login')); +}); + +test('guest cannot delete supplier', function () { + $supplier = Supplier::factory()->create(); + + $response = $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + $response->assertRedirect(route('login')); + $this->assertDatabaseHas('suppliers', ['id' => $supplier->id, 'deleted_at' => null]); +}); + +/* +|-------------------------------------------------------------------------- +| DATA INTEGRITY +|-------------------------------------------------------------------------- +*/ + +test('created supplier has correct timestamps', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Timestamp Test', + ]); + + $supplier = Supplier::where('name', 'Timestamp Test')->first(); + expect($supplier->created_at)->not->toBeNull(); + expect($supplier->updated_at)->not->toBeNull(); +}); + +test('updated supplier has updated timestamp changed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + $originalUpdatedAt = $supplier->updated_at; + + sleep(1); + + $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Timestamp Changed', + ]); + + $supplier->refresh(); + expect($supplier->updated_at->gt($originalUpdatedAt))->toBeTrue(); +}); + +test('supplier ID is auto-incremented', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'First']); + $first = Supplier::where('name', 'First')->first(); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Second']); + $second = Supplier::where('name', 'Second')->first(); + + expect($second->id)->toBe($first->id + 1); +}); + +test('supplier factory creates valid supplier', function () { + $supplier = Supplier::factory()->create(); + + expect($supplier->name)->not->toBeEmpty(); + expect($supplier->id)->toBeInt(); +}); + +test('supplier factory creates unique names', function () { + $suppliers = Supplier::factory()->count(20)->create(); + + $names = $suppliers->pluck('name')->toArray(); + expect($names)->toHaveCount(count(array_unique($names))); +}); + +/* +|-------------------------------------------------------------------------- +| SOFT DELETE & RESTORE +|-------------------------------------------------------------------------- +*/ + +test('supplier can be restored after soft delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['name' => 'Restored Supplier']); + $supplier->delete(); + + $this->assertSoftDeleted('suppliers', ['id' => $supplier->id]); + + $supplier->restore(); + + $this->assertDatabaseHas('suppliers', ['id' => $supplier->id, 'deleted_at' => null]); +}); + +test('restored supplier appears in index again', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['name' => 'Restored Supplier']); + $supplier->delete(); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 0) + ); + + $supplier->restore(); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 1) + ->where('suppliers.0.name', 'Restored Supplier') + ); +}); + +test('restored supplier can be updated', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(['name' => 'Before Restore']); + $supplier->delete(); + $supplier->restore(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'After Restore', + ]); + + $response->assertSessionHasNoErrors(); + + $supplier->refresh(); + expect($supplier->name)->toBe('After Restore'); +}); + +/* +|-------------------------------------------------------------------------- +| REALISTIC USER SCENARIOS +|-------------------------------------------------------------------------- +*/ + +test('user creates supplier then immediately edits it', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Draft Supplier']); + $supplier = Supplier::where('name', 'Draft Supplier')->first(); + + $this->put(route('admin.master.suppliers.update', $supplier), ['name' => 'Final Supplier']); + + $supplier->refresh(); + expect($supplier->name)->toBe('Final Supplier'); +}); + +test('user creates multiple suppliers and deletes one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Keep']); + $this->post(route('admin.master.suppliers.store'), ['name' => 'Delete Me']); + $this->post(route('admin.master.suppliers.store'), ['name' => 'Also Keep']); + + $toDelete = Supplier::where('name', 'Delete Me')->first(); + $this->delete(route('admin.master.suppliers.destroy', $toDelete)); + + $response = $this->get(route('admin.master.suppliers.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/supplier/index') + ->has('suppliers', 2) + ); +}); + +test('user renames supplier to match another existing supplier name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Supplier::factory()->create(['name' => 'Target Name']); + $supplier = Supplier::factory()->create(['name' => 'Source Name']); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), [ + 'name' => 'Target Name', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('user creates supplier with Indonesian characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'PT Maju Jaya Sentosa', + 'phone_number' => '0215551234', + 'address' => 'Jl. Sudirman No. 123, Jakarta Selatan', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'PT Maju Jaya Sentosa', + 'phone_number' => '0215551234', + 'address' => 'Jl. Sudirman No. 123, Jakarta Selatan', + ]); +}); + +test('user tries to create supplier without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user tries to update supplier without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $supplier = Supplier::factory()->create(); + + $response = $this->put(route('admin.master.suppliers.update', $supplier), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user rapidly submits same supplier creation twice', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Rapid Submit']); + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Rapid Submit']); + + $response->assertSessionHasErrors('name'); + $this->assertDatabaseCount('suppliers', 1); +}); + +test('user creates supplier with name containing chinese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => '电子产品供应商']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing japanese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'サプライヤー']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing korean characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => '공급업체']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing arabic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'المورد']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing cyrillic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Поставщик']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing thai characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'ซัพพลายเออร์']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing hindi characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'आपूर्तिकर्ता']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing greek characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Προμηθευτής']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing vietnamese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Nhà cung cấp']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing mixed unicode scripts', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier 电子产品']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing emoji', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => '🏢 Supplier']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with very short name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'X']); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'X', + ]); +}); + +test('user creates supplier with name containing mixed case and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'PT123ABC456']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'PT123ABC456', + ]); +}); + +test('user creates supplier with name containing only spaces and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => ' 123 ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with very long name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 200); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => $name]); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => $name, + ]); +}); + +test('user creates supplier then creates another with similar name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier ABC']); + $this->post(route('admin.master.suppliers.store'), ['name' => 'Supplier XYZ']); + + $this->assertDatabaseCount('suppliers', 2); + $this->assertDatabaseHas('suppliers', ['name' => 'Supplier ABC']); + $this->assertDatabaseHas('suppliers', ['name' => 'Supplier XYZ']); +}); + +test('user creates supplier with name containing curly braces', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => '{code}']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '{code}', + ]); +}); + +test('user creates supplier with name containing angle brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => '']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '', + ]); +}); + +test('user creates supplier with name containing semicolon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'A;B']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'A;B', + ]); +}); + +test('user creates supplier with name containing colon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Time: Now']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Time: Now', + ]); +}); + +test('user creates supplier with name containing backtick', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => '`Code`']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '`Code`', + ]); +}); + +test('user creates supplier with name containing tilde', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Caf~']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Caf~', + ]); +}); + +test('user creates supplier with name containing caret', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'A^B']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'A^B', + ]); +}); + +test('user creates supplier with name containing percentage', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => '100% Natural']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => '100% Natural', + ]); +}); + +test('user creates supplier with name containing fullwidth characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => "ABCDE"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing fullwidth brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => "()【】"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing hiragana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'あいうえお']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing katakana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'アイウエオ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing hangul', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => '가나다라마']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing hebrew', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'אבגדהו']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing bengali', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'অআইঈউ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing tamil', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'அஆஇஈஉ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates supplier with name containing email-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'user@example.com']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'user@example.com', + ]); +}); + +test('user creates supplier with name containing URL-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'https://example.com']); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'https://example.com', + ]); +}); + +test('user creates supplier with address containing Indonesian address format', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier Alamat', + 'address' => "Jl. Raya Bogor Km. 30\nRT 01/02\nKel. Sukamaju\nKec. Menteng\nJakarta Pusat 10310", + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier Alamat', + ]); +}); + +test('user creates supplier with phone number in various Indonesian formats', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $phones = [ + '081234567890', + '6281234567890', + '0215551234', + '02155512345', + ]; + + foreach ($phones as $phone) { + $response = $this->post(route('admin.master.suppliers.store'), [ + 'name' => 'Supplier ' . $phone, + 'phone_number' => $phone, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('suppliers', [ + 'name' => 'Supplier ' . $phone, + 'phone_number' => $phone, + ]); + } +}); + +test('user creates supplier then deletes it and creates new one with same name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.suppliers.store'), ['name' => 'Reusable']); + $supplier = Supplier::where('name', 'Reusable')->first(); + + $this->delete(route('admin.master.suppliers.destroy', $supplier)); + + // Soft-deleted name still triggers unique validation + $response = $this->post(route('admin.master.suppliers.store'), ['name' => 'Reusable']); + $response->assertSessionHasErrors('name'); +}); + +test('user creates supplier with name containing regex special characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.suppliers.store'), ['name' => '.*+?^${}()|[]\\']); + $response->assertSessionHasNoErrors(); +});