diff --git a/tests/Feature/Admin/Master/CategoryTest.php b/tests/Feature/Admin/Master/CategoryTest.php index f4d7d7f..37011eb 100644 --- a/tests/Feature/Admin/Master/CategoryTest.php +++ b/tests/Feature/Admin/Master/CategoryTest.php @@ -1,9 +1,21 @@ get(route('admin.master.categories.index')); $response->assertRedirect(route('login')); @@ -17,11 +29,17 @@ $response->assertOk(); }); +/* +|-------------------------------------------------------------------------- +| INDEX PAGE +|-------------------------------------------------------------------------- +*/ + test('category index page displays categories', function () { $user = User::factory()->create(); $this->actingAs($user); - $categories = Category::factory()->count(3)->create(); + Category::factory()->count(3)->create(); $response = $this->get(route('admin.master.categories.index')); $response->assertOk(); @@ -31,6 +49,57 @@ ); }); +test('index page works with zero categories', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 0) + ); +}); + +test('index page does not display soft-deleted categories', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Category::factory()->create(['name' => 'Active Category']); + Category::factory()->create(['name' => 'Deleted Category'])->delete(); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 1) + ->where('categories.0.name', 'Active Category') + ); +}); + +test('index page displays correct count after delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Category::factory()->count(5)->create(); + $category = Category::first(); + + $this->delete(route('admin.master.categories.destroy', $category)); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 4) + ); +}); + +/* +|-------------------------------------------------------------------------- +| CREATE / STORE +|-------------------------------------------------------------------------- +*/ + test('category can be created', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -49,6 +118,20 @@ ]); }); +test('category slug is automatically generated from name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori Otomatis', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori Otomatis', + 'slug' => 'kategori-otomatis', + ]); +}); + test('category name is required', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -60,6 +143,17 @@ $response->assertSessionHasErrors('name'); }); +test('category name must be string', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 12345, + ]); + + $response->assertSessionHasErrors('name'); +}); + test('category name must not exceed 100 characters', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -71,6 +165,33 @@ $response->assertSessionHasErrors('name'); }); +test('category name exactly 100 characters passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => str_repeat('a', 100), + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('category name exactly 1 character passes validation', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'A', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A', + 'slug' => 'a', + ]); +}); + test('category name must be unique', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -84,20 +205,253 @@ $response->assertSessionHasErrors('name'); }); -test('category slug is automatically generated', function () { +test('category name uniqueness is case sensitive', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Category::factory()->create(['name' => 'Makanan']); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Makanan', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('category name with only whitespace is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('category name with special characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Makanan & Minuman!', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Makanan & Minuman!', + 'slug' => 'makanan-minuman', + ]); +}); + +test('category name with HTML tags is accepted as plain text', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => '', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => '', + ]); +}); + +test('category name with numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori 123', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori 123', + 'slug' => 'kategori-123', + ]); +}); + +test('category name with only numbers is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => '12345', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => '12345', + 'slug' => '12345', + ]); +}); + +test('category name with only punctuation is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => '---', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('category name with parentheses and brackets is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori (A) [VIP]', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori (A) [VIP]', + 'slug' => 'kategori-a-vip', + ]); +}); + +test('category name with unicode characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori Nono', + ]); +}); + +test('category name with mixed case is stored as entered', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'makanAN MinUMAN', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'makanAN MinUMAN', + 'slug' => 'makanan-minuman', + ]); +}); + +test('category name with multiple spaces generates correct slug', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori dengan spasi banyak', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Kategori dengan spasi banyak', + 'slug' => 'kategori-dengan-spasi-banyak', + ]); +}); + +test('category name with leading and trailing spaces is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => ' Kategori Spasi ', + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('category name with newline characters is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => "Kategori\nBaru", + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('category can be created with name at exact DB column limit (50 chars)', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 50); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => $name, + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => $name, + ]); +}); + +test('store flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Kategori Toast', + ]); + + $response->assertRedirect(); +}); + +test('store creates new category in database', function () { $user = User::factory()->create(); $this->actingAs($user); $this->post(route('admin.master.categories.store'), [ - 'name' => 'Kategori Otomatis', + 'name' => 'Database Check', ]); + $this->assertDatabaseCount('categories', 1); $this->assertDatabaseHas('categories', [ - 'name' => 'Kategori Otomatis', - 'slug' => 'kategori-otomatis', + 'name' => 'Database Check', + 'slug' => 'database-check', ]); }); +test('multiple categories can be created sequentially', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Kategori 1']); + $this->post(route('admin.master.categories.store'), ['name' => 'Kategori 2']); + $this->post(route('admin.master.categories.store'), ['name' => 'Kategori 3']); + + $this->assertDatabaseCount('categories', 3); + + $this->assertDatabaseHas('categories', ['name' => 'Kategori 1', 'slug' => 'kategori-1']); + $this->assertDatabaseHas('categories', ['name' => 'Kategori 2', 'slug' => 'kategori-2']); + $this->assertDatabaseHas('categories', ['name' => 'Kategori 3', 'slug' => 'kategori-3']); +}); + +/* +|-------------------------------------------------------------------------- +| UPDATE / PUT +|-------------------------------------------------------------------------- +*/ + test('category can be updated', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -144,6 +498,136 @@ $response->assertSessionHasErrors('name'); }); +test('category update name is required', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => '', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('category update name must not exceed 100 characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => str_repeat('a', 101), + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('category update with special characters generates correct slug', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Old Name']); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'New Name (Updated) & More!', + ]); + + $response->assertSessionHasNoErrors(); + + $category->refresh(); + expect($category->slug)->toBe('new-name-updated-more'); +}); + +test('category update changes the slug to match new name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Original', 'slug' => 'original']); + + $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Modified', + ]); + + $category->refresh(); + expect($category->slug)->toBe('modified'); +}); + +test('category can be updated multiple times', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Version 1']); + + $this->put(route('admin.master.categories.update', $category), ['name' => 'Version 2']); + $category->refresh(); + expect($category->name)->toBe('Version 2'); + + $this->put(route('admin.master.categories.update', $category), ['name' => 'Version 3']); + $category->refresh(); + expect($category->name)->toBe('Version 3'); +}); + +test('update flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Updated Name', + ]); + + $response->assertRedirect(); +}); + +test('category update with only whitespace is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => ' ', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('category can be updated with unicode characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Original']); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Kategori Nono', + ]); + + $response->assertSessionHasNoErrors(); + + $category->refresh(); + expect($category->name)->toBe('Kategori Nono'); +}); + +test('updating non-existent category returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->put(route('admin.master.categories.update', 999999), [ + 'name' => 'Ghost Category', + ]); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| DELETE / DESTROY +|-------------------------------------------------------------------------- +*/ + test('category can be deleted', function () { $user = User::factory()->create(); $this->actingAs($user); @@ -158,3 +642,1181 @@ $this->assertSoftDeleted('categories', ['id' => $category->id]); }); + +test('category is soft-deleted not permanently removed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $this->delete(route('admin.master.categories.destroy', $category)); + + $this->assertDatabaseHas('categories', ['id' => $category->id]); + $this->assertSoftDeleted('categories', ['id' => $category->id]); +}); + +test('soft-deleted category still exists in database', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $this->delete(route('admin.master.categories.destroy', $category)); + + $this->assertDatabaseHas('categories', ['id' => $category->id]); +}); + +test('soft-deleted category has deleted_at timestamp', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $this->delete(route('admin.master.categories.destroy', $category)); + + $category->refresh(); + expect($category->deleted_at)->not->toBeNull(); +}); + +test('deleted category is excluded from getAll query', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Category::factory()->create(['name' => 'Active']); + $deleted = Category::factory()->create(['name' => 'Deleted']); + $deleted->delete(); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 1) + ->where('categories.0.name', 'Active') + ); +}); + +test('delete flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->delete(route('admin.master.categories.destroy', $category)); + + $response->assertRedirect(); +}); + +test('multiple categories can be deleted one by one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $categories = Category::factory()->count(3)->create(); + + $this->delete(route('admin.master.categories.destroy', $categories[0])); + $this->delete(route('admin.master.categories.destroy', $categories[1])); + + $this->assertSoftDeleted('categories', ['id' => $categories[0]->id]); + $this->assertSoftDeleted('categories', ['id' => $categories[1]->id]); + $this->assertDatabaseHas('categories', ['id' => $categories[2]->id, 'deleted_at' => null]); +}); + +test('updating soft-deleted category via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $category->delete(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Should Not Work', + ]); + + $response->assertStatus(404); +}); + +test('deleting soft-deleted category via route returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $category->delete(); + + $response = $this->delete(route('admin.master.categories.destroy', $category)); + + $response->assertStatus(404); +}); + +test('deleting non-existent category returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->delete(route('admin.master.categories.destroy', 999999)); + + $response->assertStatus(404); +}); + +/* +|-------------------------------------------------------------------------- +| CATEGORY - PRODUCT RELATIONSHIP +|-------------------------------------------------------------------------- +*/ + +test('category with products can be deleted without deleting products', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $product = Product::factory()->create(); + DB::table('product_categories')->insert([ + 'product_id' => $product->id, + 'category_id' => $category->id, + ]); + + $this->delete(route('admin.master.categories.destroy', $category)); + + $this->assertSoftDeleted('categories', ['id' => $category->id]); + $this->assertDatabaseHas('products', ['id' => $product->id, 'deleted_at' => null]); +}); + +test('category-product pivot record remains after soft delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $product = Product::factory()->create(); + DB::table('product_categories')->insert([ + 'product_id' => $product->id, + 'category_id' => $category->id, + ]); + + $this->assertDatabaseHas('product_categories', [ + 'category_id' => $category->id, + 'product_id' => $product->id, + ]); + + $this->delete(route('admin.master.categories.destroy', $category)); + + // Soft delete doesn't trigger cascade delete, so pivot record remains + $this->assertDatabaseHas('product_categories', [ + 'category_id' => $category->id, + 'product_id' => $product->id, + ]); +}); + +test('index page works correctly with categories that have products', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $products = Product::factory()->count(3)->create(); + foreach ($products as $product) { + DB::table('product_categories')->insert([ + 'product_id' => $product->id, + 'category_id' => $category->id, + ]); + } + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 1) + ); +}); + +test('category with many products can be deleted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $products = Product::factory()->count(10)->create(); + foreach ($products as $product) { + DB::table('product_categories')->insert([ + 'product_id' => $product->id, + 'category_id' => $category->id, + ]); + } + + $this->delete(route('admin.master.categories.destroy', $category)); + + $this->assertSoftDeleted('categories', ['id' => $category->id]); + $this->assertDatabaseCount('products', 10); +}); + +test('deleting category does not affect other categories products belong to', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category1 = Category::factory()->create(); + $category2 = Category::factory()->create(); + $product = Product::factory()->create(); + DB::table('product_categories')->insert([ + ['product_id' => $product->id, 'category_id' => $category1->id], + ['product_id' => $product->id, 'category_id' => $category2->id], + ]); + + $this->delete(route('admin.master.categories.destroy', $category1)); + + $this->assertSoftDeleted('categories', ['id' => $category1->id]); + $this->assertDatabaseHas('categories', ['id' => $category2->id, 'deleted_at' => null]); + $this->assertDatabaseHas('product_categories', [ + 'product_id' => $product->id, + 'category_id' => $category2->id, + ]); +}); + +/* +|-------------------------------------------------------------------------- +| SLUG EDGE CASES +|-------------------------------------------------------------------------- +*/ + +test('slug is generated correctly from name with ampersand', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Food & Beverage', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Food & Beverage', + 'slug' => Str::slug('Food & Beverage'), + ]); +}); + +test('slug is generated correctly from name with dots', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Mr. Category', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Mr. Category', + 'slug' => Str::slug('Mr. Category'), + ]); +}); + +test('slug is generated correctly from name with slashes', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Food/Drink', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Food/Drink', + 'slug' => Str::slug('Food/Drink'), + ]); +}); + +test('slug is generated correctly from name with commas', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Makanan, Minuman', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Makanan, Minuman', + 'slug' => Str::slug('Makanan, Minuman'), + ]); +}); + +test('slug is generated correctly from name with single quotes', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => "It's Category", + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => "It's Category", + 'slug' => Str::slug("It's Category"), + ]); +}); + +test('slug is generated correctly from name with double quotes', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => '"Premium" Category', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => '"Premium" Category', + 'slug' => Str::slug('"Premium" Category'), + ]); +}); + +test('slug is generated correctly from name with exclamation mark', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Hot Sale!', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Hot Sale!', + 'slug' => Str::slug('Hot Sale!'), + ]); +}); + +test('slug is generated correctly from name with question mark', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Best Choice?', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Best Choice?', + 'slug' => Str::slug('Best Choice?'), + ]); +}); + +test('slug is generated correctly from all-caps name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'ELECTRONICS', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'ELECTRONICS', + 'slug' => Str::slug('ELECTRONICS'), + ]); +}); + +test('slug is generated correctly from name with numbers and letters mixed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'iPhone 15 Pro', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'iPhone 15 Pro', + 'slug' => Str::slug('iPhone 15 Pro'), + ]); +}); + +test('slug is generated correctly from name with dash', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Food-Grade', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Food-Grade', + 'slug' => Str::slug('Food-Grade'), + ]); +}); + +test('slug is generated correctly from name with underscore', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Food_Grade', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Food_Grade', + 'slug' => Str::slug('Food_Grade'), + ]); +}); + +test('slug is generated correctly from name with at symbol', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Category@Home', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Category@Home', + 'slug' => Str::slug('Category@Home'), + ]); +}); + +test('slug is generated correctly from name with hash symbol', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Category#1', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Category#1', + 'slug' => Str::slug('Category#1'), + ]); +}); + +test('slug is generated correctly from name with currency symbol', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Rp 50.000', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Rp 50.000', + 'slug' => Str::slug('Rp 50.000'), + ]); +}); + +test('slug is generated correctly from name with pipe symbol', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'A|B', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A|B', + 'slug' => Str::slug('A|B'), + ]); +}); + +test('slug is generated correctly from name with mixed special chars', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'A & B (v2.0)!', + ]); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A & B (v2.0)!', + 'slug' => Str::slug('A & B (v2.0)!'), + ]); +}); + +test('slug does not change when category is updated with same name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Same Name', 'slug' => 'same-name']); + $originalSlug = $category->slug; + + $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Same Name', + ]); + + $category->refresh(); + expect($category->slug)->toBe($originalSlug); +}); + +test('slug changes when category name changes', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Old Name', 'slug' => 'old-name']); + + $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'New Name', + ]); + + $category->refresh(); + expect($category->slug)->toBe('new-name'); + expect($category->slug)->not->toBe('old-name'); +}); + +/* +|-------------------------------------------------------------------------- +| AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS +|-------------------------------------------------------------------------- +*/ + +test('guest cannot create category', function () { + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Unauthorized', + ]); + + $response->assertRedirect(route('login')); + $this->assertDatabaseMissing('categories', ['name' => 'Unauthorized']); +}); + +test('guest cannot update category', function () { + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Unauthorized Update', + ]); + + $response->assertRedirect(route('login')); +}); + +test('guest cannot delete category', function () { + $category = Category::factory()->create(); + + $response = $this->delete(route('admin.master.categories.destroy', $category)); + + $response->assertRedirect(route('login')); + $this->assertDatabaseHas('categories', ['id' => $category->id, 'deleted_at' => null]); +}); + +/* +|-------------------------------------------------------------------------- +| DATA INTEGRITY +|-------------------------------------------------------------------------- +*/ + +test('created category has correct timestamps', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), [ + 'name' => 'Timestamp Test', + ]); + + $category = Category::where('name', 'Timestamp Test')->first(); + expect($category->created_at)->not->toBeNull(); + expect($category->updated_at)->not->toBeNull(); +}); + +test('updated category has updated timestamp changed', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + $originalUpdatedAt = $category->updated_at; + + sleep(1); + + $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Timestamp Changed', + ]); + + $category->refresh(); + expect($category->updated_at->gt($originalUpdatedAt))->toBeTrue(); +}); + +test('category ID is auto-incremented', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'First']); + $first = Category::where('name', 'First')->first(); + + $this->post(route('admin.master.categories.store'), ['name' => 'Second']); + $second = Category::where('name', 'Second')->first(); + + expect($second->id)->toBe($first->id + 1); +}); + +test('category factory creates valid category', function () { + $category = Category::factory()->create(); + + expect($category->name)->not->toBeEmpty(); + expect($category->slug)->not->toBeEmpty(); + expect($category->id)->toBeInt(); +}); + +test('category factory creates unique names', function () { + $categories = Category::factory()->count(20)->create(); + + $names = $categories->pluck('name')->toArray(); + expect($names)->toHaveCount(count(array_unique($names))); +}); + +/* +|-------------------------------------------------------------------------- +| SOFT DELETE & RESTORE +|-------------------------------------------------------------------------- +*/ + +test('category can be restored after soft delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Restored Category']); + $category->delete(); + + $this->assertSoftDeleted('categories', ['id' => $category->id]); + + $category->restore(); + + $this->assertDatabaseHas('categories', ['id' => $category->id, 'deleted_at' => null]); +}); + +test('restored category appears in index again', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Restored Category']); + $category->delete(); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 0) + ); + + $category->restore(); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 1) + ->where('categories.0.name', 'Restored Category') + ); +}); + +test('restored category can be updated', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(['name' => 'Before Restore']); + $category->delete(); + $category->restore(); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'After Restore', + ]); + + $response->assertSessionHasNoErrors(); + + $category->refresh(); + expect($category->name)->toBe('After Restore'); +}); + +/* +|-------------------------------------------------------------------------- +| REALISTIC USER SCENARIOS +|-------------------------------------------------------------------------- +*/ + +test('user creates category then immediately edits it', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Draft Category']); + $category = Category::where('name', 'Draft Category')->first(); + + $this->put(route('admin.master.categories.update', $category), ['name' => 'Final Category']); + + $category->refresh(); + expect($category->name)->toBe('Final Category'); + expect($category->slug)->toBe('final-category'); +}); + +test('user creates multiple categories and deletes one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Keep']); + $this->post(route('admin.master.categories.store'), ['name' => 'Delete Me']); + $this->post(route('admin.master.categories.store'), ['name' => 'Also Keep']); + + $toDelete = Category::where('name', 'Delete Me')->first(); + $this->delete(route('admin.master.categories.destroy', $toDelete)); + + $response = $this->get(route('admin.master.categories.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/master/category/index') + ->has('categories', 2) + ); +}); + +test('user renames category to match another existing category name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Category::factory()->create(['name' => 'Target Name']); + $category = Category::factory()->create(['name' => 'Source Name']); + + $response = $this->put(route('admin.master.categories.update', $category), [ + 'name' => 'Target Name', + ]); + + $response->assertSessionHasErrors('name'); +}); + +test('user creates category with Indonesian characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), [ + 'name' => 'Makanan Khas Jawa', + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Makanan Khas Jawa', + 'slug' => 'makanan-khas-jawa', + ]); +}); + +test('user tries to create category without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user tries to update category without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $category = Category::factory()->create(); + + $response = $this->put(route('admin.master.categories.update', $category), []); + + $response->assertSessionHasErrors('name'); +}); + +test('user rapidly submits same category creation twice', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Rapid Submit']); + $response = $this->post(route('admin.master.categories.store'), ['name' => 'Rapid Submit']); + + $response->assertSessionHasErrors('name'); + $this->assertDatabaseCount('categories', 1); +}); + +test('user creates category with name containing chinese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => '电子产品']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing japanese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'カテゴリ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing korean characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => '카테고리']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing arabic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'الفئة']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing cyrillic characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'Электроника']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing thai characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'หมวดหมู่']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing hindi characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'श्रेणी']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing greek characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'Κατηγορία']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing vietnamese characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'Danh Mục']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing mixed unicode scripts', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'Food 电子产品']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing email-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'user@example.com']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'user@example.com', + 'slug' => Str::slug('user@example.com'), + ]); +}); + +test('user creates category with name containing URL-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'https://example.com']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'https://example.com', + 'slug' => Str::slug('https://example.com'), + ]); +}); + +test('user creates category with name containing date-like pattern', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => '2024-01-15']); + + $this->assertDatabaseHas('categories', [ + 'name' => '2024-01-15', + 'slug' => '2024-01-15', + ]); +}); + +test('user creates category with name containing emoji', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => '🔥 Hot Category']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with very short name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'X']); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => 'X', + 'slug' => 'x', + ]); +}); + +test('user creates category with name containing multiple dashes', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'A--B']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A--B', + 'slug' => Str::slug('A--B'), + ]); +}); + +test('user creates category with name containing mixed case and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'iPhone15ProMax']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'iPhone15ProMax', + 'slug' => Str::slug('iPhone15ProMax'), + ]); +}); + +test('user creates category with name containing only spaces and numbers', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => ' 123 ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing very long single word', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $name = str_repeat('a', 50); + + $response = $this->post(route('admin.master.categories.store'), ['name' => $name]); + $response->assertSessionHasNoErrors(); + + $this->assertDatabaseHas('categories', [ + 'name' => $name, + 'slug' => $name, + ]); +}); + +test('user creates category then creates another with similar name', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Makanan']); + $this->post(route('admin.master.categories.store'), ['name' => 'Minuman']); + + $this->assertDatabaseCount('categories', 2); + $this->assertDatabaseHas('categories', ['name' => 'Makanan', 'slug' => 'makanan']); + $this->assertDatabaseHas('categories', ['name' => 'Minuman', 'slug' => 'minuman']); +}); + +test('user creates category with name containing curly braces', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => '{code}']); + + $this->assertDatabaseHas('categories', [ + 'name' => '{code}', + 'slug' => Str::slug('{code}'), + ]); +}); + +test('user creates category with name containing angle brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => '']); + + $this->assertDatabaseHas('categories', [ + 'name' => '', + 'slug' => Str::slug(''), + ]); +}); + +test('user creates category with name containing semicolon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'A;B']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A;B', + 'slug' => Str::slug('A;B'), + ]); +}); + +test('user creates category with name containing colon', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Time: Now']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Time: Now', + 'slug' => Str::slug('Time: Now'), + ]); +}); + +test('user creates category with name containing backtick', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => '`Code`']); + + $this->assertDatabaseHas('categories', [ + 'name' => '`Code`', + 'slug' => Str::slug('`Code`'), + ]); +}); + +test('user creates category with name containing tilde', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'Caf~']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Caf~', + 'slug' => Str::slug('Caf~'), + ]); +}); + +test('user creates category with name containing caret', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'A^B']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A^B', + 'slug' => Str::slug('A^B'), + ]); +}); + +test('user creates category with name containing percentage', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => '100% Natural']); + + $this->assertDatabaseHas('categories', [ + 'name' => '100% Natural', + 'slug' => Str::slug('100% Natural'), + ]); +}); + +test('user creates category with name containing regex special characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => '.*+?^${}()|[]\\']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing fullwidth characters', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => "ABCDE"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing fullwidth brackets', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => "()【】"]); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing hiragana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'あいうえお']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing katakana', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'アイウエオ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing hangul', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => '가나다라마']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing hebrew', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'אבגדהו']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing bengali', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'অআইঈউ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing tamil', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'அஆஇஈஉ']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing superscript', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => 'E=mc2']); + $response->assertSessionHasNoErrors(); +}); + +test('user creates category with name containing only newlines', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => "\n\n\n"]); + $response->assertSessionHasErrors('name'); +}); + +test('user creates category with name containing only tabs', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => "\t\t\t"]); + $response->assertSessionHasErrors('name'); +}); + +test('user creates category with name containing mixed whitespace and visible chars', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.master.categories.store'), ['name' => " \t Hello \n World "]); + $response->assertSessionHasNoErrors(); + + $category = DB::table('categories')->first(); + expect($category->name)->not->toBeEmpty(); +}); + +test('user creates category with name containing plus sign', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'A+B']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A+B', + 'slug' => Str::slug('A+B'), + ]); +}); + +test('user creates category with name containing equals sign', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $this->post(route('admin.master.categories.store'), ['name' => 'A=B']); + + $this->assertDatabaseHas('categories', [ + 'name' => 'A=B', + 'slug' => Str::slug('A=B'), + ]); +});