refactor: update database connection settings in phpunit.xml and clean up CategoryService by removing unused docblocks and reorganizing sorting logic for improved clarity

This commit is contained in:
Yoga Pangestu 2026-06-22 15:02:04 +07:00
parent 27c5b5fffa
commit 4f7290711d
4 changed files with 349 additions and 29 deletions

View File

@ -8,9 +8,6 @@
class CategoryService class CategoryService
{ {
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{ {
$query = Category::query() $query = Category::query()
@ -28,17 +25,11 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
->withQueryString(); ->withQueryString();
} }
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated): void public function create(array $validated): void
{ {
Category::create($validated); Category::create($validated);
} }
/**
* @param array<string, mixed> $validated
*/
public function update(Category $category, array $validated): void public function update(Category $category, array $validated): void
{ {
$category->update($validated); $category->update($validated);
@ -49,6 +40,18 @@ public function delete(Category $category): void
$category->delete(); $category->delete();
} }
public function getSelectOptions(): array
{
return Category::query()
->orderBy('name')
->get(['id', 'name'])
->map(fn (Category $category) => [
'value' => $category->id,
'label' => $category->name,
])
->all();
}
private function applySorting(Builder $query, string $sort, string $direction): void private function applySorting(Builder $query, string $sort, string $direction): void
{ {
if (in_array($sort, ['name', 'slug'], true)) { if (in_array($sort, ['name', 'slug'], true)) {
@ -59,19 +62,4 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest(); $query->latest();
} }
/**
* @return list<array{value: int, label: string}>
*/
public function getSelectOptions(): array
{
return Category::query()
->orderBy('name')
->get(['id', 'name'])
->map(fn (Category $category) => [
'value' => $category->id,
'label' => $category->name,
])
->all();
}
} }

View File

@ -23,9 +23,8 @@
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/> <env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/> <env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/> <env name="DB_CONNECTION" value="mysql"/>
<env name="DB_DATABASE" value=":memory:"/> <env name="DB_DATABASE" value="pangestu-dstcollection-store-testing"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/> <env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/> <env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>

View File

@ -2,8 +2,6 @@
import { Head } from '@inertiajs/vue3'; import { Head } from '@inertiajs/vue3';
import { Plus } from '@lucide/vue'; import { Plus } from '@lucide/vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import CategoryFormModal from './form/CategoryFormModal.vue';
import { createColumns } from './table/columns';
import { DataTable } from '@/components/data-table'; import { DataTable } from '@/components/data-table';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
@ -12,6 +10,8 @@ import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataT
import AdminLayout from '@/layouts/AdminLayout.vue'; import AdminLayout from '@/layouts/AdminLayout.vue';
import type { CategoryListItem, PaginatedCategories } from '@/types/category'; import type { CategoryListItem, PaginatedCategories } from '@/types/category';
import type { DataTableSort } from '@/types/data-table'; import type { DataTableSort } from '@/types/data-table';
import CategoryFormModal from './form/CategoryFormModal.vue';
import { createColumns } from './table/columns';
const props = defineProps<{ const props = defineProps<{
categories: PaginatedCategories; categories: PaginatedCategories;

View File

@ -0,0 +1,333 @@
<?php
use App\Enums\Permission as PermissionEnum;
use App\Models\Category;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
});
// ─── Helper ───────────────────────────────────────────────
function createUserWithPermission(PermissionEnum ...$permissions): User
{
$user = User::factory()->create();
$user->givePermissionTo(
array_merge(
[PermissionEnum::DASHBOARD_VIEW->value],
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
)
);
$user->forgetCachedPermissions();
return $user;
}
// ─── Index ────────────────────────────────────────────────
describe('Category Index', function () {
test('authenticated user with permission can view category index', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
Category::factory()->count(3)->create();
$this->actingAs($user)
->get(route('admin.master.categories.index'))
->assertOk();
});
test('guest is redirected to login', function () {
$this->get(route('admin.master.categories.index'))
->assertRedirect(route('login'));
});
test('user without permission is forbidden', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('admin.master.categories.index'))
->assertForbidden();
});
test('index returns categories with pagination', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
Category::factory()->count(15)->create();
$response = $this->actingAs($user)
->get(route('admin.master.categories.index'));
$response->assertOk();
});
test('index can search categories by name', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
Category::factory()->create(['name' => 'Kaos Polos']);
Category::factory()->create(['name' => 'Kemeja Formal']);
$this->actingAs($user)
->get(route('admin.master.categories.index', ['search' => 'Kaos']))
->assertOk();
});
});
// ─── Store ────────────────────────────────────────────────
describe('Category Store', function () {
test('authenticated user with permission can create a category', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => 'Kategori Baru',
])
->assertRedirect(route('admin.master.categories.index'));
$this->assertDatabaseHas('categories', [
'name' => 'Kategori Baru',
'slug' => 'kategori-baru',
]);
});
test('guest cannot create a category', function () {
$this->post(route('admin.master.categories.store'), [
'name' => 'Kategori Baru',
])->assertRedirect(route('login'));
});
test('user without create permission is forbidden', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => 'Kategori Baru',
])
->assertForbidden();
$this->assertDatabaseCount('categories', 0);
});
test('name is required', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => '',
])
->assertSessionHasErrors('name');
$this->assertDatabaseCount('categories', 0);
});
test('name must not exceed 50 characters', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => str_repeat('a', 51),
])
->assertSessionHasErrors('name');
$this->assertDatabaseCount('categories', 0);
});
test('name must be a string', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => ['bukan string'],
])
->assertSessionHasErrors('name');
$this->assertDatabaseCount('categories', 0);
});
test('slug is auto-generated from name', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
$this->actingAs($user)
->post(route('admin.master.categories.store'), [
'name' => 'Bahan Katun',
]);
$this->assertDatabaseHas('categories', [
'name' => 'Bahan Katun',
'slug' => 'bahan-katun',
]);
});
});
// ─── Update ───────────────────────────────────────────────
describe('Category Update', function () {
test('authenticated user with permission can update a category', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_UPDATE);
$category = Category::factory()->create(['name' => 'Nama Lama']);
$this->actingAs($user)
->put(route('admin.master.categories.update', $category), [
'name' => 'Nama Baru',
])
->assertRedirect(route('admin.master.categories.index'));
$this->assertDatabaseHas('categories', [
'id' => $category->id,
'name' => 'Nama Baru',
]);
});
test('guest cannot update a category', function () {
$category = Category::factory()->create();
$this->put(route('admin.master.categories.update', $category), [
'name' => 'Nama Baru',
])->assertRedirect(route('login'));
});
test('user without update permission is forbidden', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
$category = Category::factory()->create(['name' => 'Nama Lama']);
$this->actingAs($user)
->put(route('admin.master.categories.update', $category), [
'name' => 'Nama Baru',
])
->assertForbidden();
$this->assertDatabaseHas('categories', [
'id' => $category->id,
'name' => 'Nama Lama',
]);
});
test('name is required on update', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_UPDATE);
$category = Category::factory()->create();
$this->actingAs($user)
->put(route('admin.master.categories.update', $category), [
'name' => '',
])
->assertSessionHasErrors('name');
});
test('name must not exceed 50 characters on update', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_UPDATE);
$category = Category::factory()->create();
$this->actingAs($user)
->put(route('admin.master.categories.update', $category), [
'name' => str_repeat('a', 51),
])
->assertSessionHasErrors('name');
});
test('slug is updated when name changes', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_UPDATE);
$category = Category::factory()->create(['name' => 'Nama Lama']);
$this->actingAs($user)
->put(route('admin.master.categories.update', $category), [
'name' => 'Nama Terbaru',
]);
$this->assertDatabaseHas('categories', [
'id' => $category->id,
'slug' => 'nama-terbaru',
]);
});
});
// ─── Destroy ──────────────────────────────────────────────
describe('Category Destroy', function () {
test('authenticated user with permission can delete a category', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_DELETE);
$category = Category::factory()->create();
$this->actingAs($user)
->delete(route('admin.master.categories.destroy', $category))
->assertRedirect(route('admin.master.categories.index'));
$this->assertSoftDeleted('categories', [
'id' => $category->id,
]);
});
test('guest cannot delete a category', function () {
$category = Category::factory()->create();
$this->delete(route('admin.master.categories.destroy', $category))
->assertRedirect(route('login'));
$this->assertNotSoftDeleted('categories', [
'id' => $category->id,
]);
});
test('user without delete permission is forbidden', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW);
$category = Category::factory()->create();
$this->actingAs($user)
->delete(route('admin.master.categories.destroy', $category))
->assertForbidden();
$this->assertNotSoftDeleted('categories', [
'id' => $category->id,
]);
});
test('deleted category is soft deleted', function () {
$user = createUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_DELETE);
$category = Category::factory()->create();
$this->actingAs($user)
->delete(route('admin.master.categories.destroy', $category));
$this->assertSoftDeleted('categories', ['id' => $category->id]);
$this->assertDatabaseHas('categories', ['id' => $category->id]);
});
});
// ─── Category Model ───────────────────────────────────────
describe('Category Model', function () {
test('category has slug auto-generated via attribute', function () {
$category = Category::factory()->create(['name' => 'Test Kategori', 'slug' => null]);
expect($category->fresh()->slug)->toBe('test-kategori');
});
test('category uses soft deletes', function () {
$category = Category::factory()->create();
$category->delete();
expect($category->trashed())->toBeTrue();
});
test('category can have products', function () {
$category = Category::factory()->create();
expect($category->products)->toBeInstanceOf(Collection::class);
});
});