forgetCachedPermissions(); $modules = [ 'user' => ['view', 'create', 'update', 'delete', 'toggle-active', 'reset-password'], 'category' => ['view', 'create', 'update', 'delete'], 'supplier' => ['view', 'create', 'update', 'delete'], 'customer' => ['view', 'create', 'update', 'delete'], 'cash-account' => ['view', 'deposit', 'withdrawal', 'update', 'delete'], 'expense' => ['view', 'create', 'update', 'delete'], 'employee-advance' => ['view', 'create', 'update', 'delete', 'approve', 'pay'], 'payroll-period' => ['view', 'show', 'current', 'close', 'reopen'], 'payroll' => ['pay', 'cancel'], 'payroll-adjustment' => ['create', 'delete'], 'leave-request' => ['view', 'create', 'update', 'delete', 'approve', 'reject'], 'attendance' => ['view', 'check-in', 'check-out', 'by-date'], 'settings' => ['view', 'update-system', 'update-homepage', 'update-social-media', 'update-hr'], ]; foreach ($modules as $module => $actions) { foreach ($actions as $action) { Permission::create(['name' => "{$module}.{$action}"]); } } $allPermissions = Permission::all()->pluck('name')->toArray(); $developer = Role::create(['name' => 'Developer']); $developer->syncPermissions($allPermissions); }); function createUserWithRole(string $roleName = 'Developer'): User { $user = User::factory()->create(); $user->assignRole($roleName); return $user; } /* |-------------------------------------------------------------------------- | AUTHENTICATION |-------------------------------------------------------------------------- */ test('guests are redirected to the login page', function () { $response = $this->get(route('admin.settings.roles.index')); $response->assertRedirect(route('login')); }); test('authenticated users without permission cannot visit the role index page', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.settings.roles.index')); $response->assertForbidden(); }); test('authenticated users with permission can visit the role index page', function () { $this->actingAs(createUserWithRole()); $response = $this->get(route('admin.settings.roles.index')); $response->assertOk(); }); /* |-------------------------------------------------------------------------- | INDEX PAGE |-------------------------------------------------------------------------- */ test('role index page displays roles', function () { $this->actingAs(createUserWithRole()); Role::create(['name' => 'Manager']); Role::create(['name' => 'Staff']); $response = $this->get(route('admin.settings.roles.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/index') ->has('roles.data', 3) ->where('roles.total', 3) ->where('roles.current_page', 1) ->where('roles.per_page', 25) ); }); test('index page works with zero additional roles', function () { $this->actingAs(createUserWithRole()); $response = $this->get(route('admin.settings.roles.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/index') ->has('roles.data', 1) ->where('roles.total', 1) ->where('roles.current_page', 1) ->where('roles.per_page', 25) ); }); test('index page displays correct role count after delete', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'To Delete']); $this->delete(route('admin.settings.roles.destroy', $role)); $response = $this->get(route('admin.settings.roles.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/index') ->has('roles.data', 1) ->where('roles.total', 1) ->where('roles.current_page', 1) ->where('roles.per_page', 25) ); }); /* |-------------------------------------------------------------------------- | CREATE / STORE |-------------------------------------------------------------------------- */ test('authenticated users without permission cannot visit the create page', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.settings.roles.create')); $response->assertForbidden(); }); test('authenticated users with permission can visit the create page', function () { $this->actingAs(createUserWithRole()); $response = $this->get(route('admin.settings.roles.create')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/create') ->has('permissions') ); }); test('role can be created', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Manager', 'permissions' => ['user.view', 'user.create'], ]); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.settings.roles.index')); $this->assertDatabaseHas('roles', ['name' => 'Manager']); }); test('role name is required', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => '', 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role name must be string', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 12345, 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role name must not exceed 100 characters', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => str_repeat('a', 101), 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role name exactly 100 characters passes validation', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => str_repeat('a', 100), 'permissions' => ['user.view'], ]); $response->assertSessionHasNoErrors(); }); test('role name must be unique', function () { $this->actingAs(createUserWithRole()); Role::create(['name' => 'Existing Role']); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Existing Role', 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role name uniqueness is case sensitive', function () { $this->actingAs(createUserWithRole()); Role::create(['name' => 'Manager']); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Manager', 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role name with special characters is accepted', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Admin & Staff!', 'permissions' => ['user.view'], ]); $response->assertSessionHasNoErrors(); $this->assertDatabaseHas('roles', ['name' => 'Admin & Staff!']); }); test('permissions are required', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'New Role', ]); $response->assertSessionHasErrors('permissions'); }); test('permissions must be an array', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'New Role', 'permissions' => 'user.view', ]); $response->assertSessionHasErrors('permissions'); }); test('permissions must exist in database', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'New Role', 'permissions' => ['nonexistent.permission'], ]); $response->assertSessionHasErrors('permissions.0'); }); test('role can be created with multiple permissions', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Multi Permission Role', 'permissions' => ['user.view', 'user.create', 'category.view'], ]); $response->assertSessionHasNoErrors(); $role = Role::where('name', 'Multi Permission Role')->first(); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.view', 'user.create', 'user.view']); }); test('role can be created with empty permissions array', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'No Permission Role', 'permissions' => [], ]); $response->assertSessionHasNoErrors(); $role = Role::where('name', 'No Permission Role')->first(); expect($role->permissions->count())->toBe(0); }); test('store flashes success toast via Inertia', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Toast Role', 'permissions' => ['user.view'], ]); $response->assertRedirect(); }); test('store creates new role in database', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), [ 'name' => 'Database Check', 'permissions' => ['user.view'], ]); $this->assertDatabaseCount('roles', 2); $this->assertDatabaseHas('roles', ['name' => 'Database Check']); }); test('multiple roles can be created sequentially', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), ['name' => 'Role 1', 'permissions' => ['user.view']]); $this->post(route('admin.settings.roles.store'), ['name' => 'Role 2', 'permissions' => ['user.view']]); $this->post(route('admin.settings.roles.store'), ['name' => 'Role 3', 'permissions' => ['user.view']]); $this->assertDatabaseCount('roles', 4); $this->assertDatabaseHas('roles', ['name' => 'Role 1']); $this->assertDatabaseHas('roles', ['name' => 'Role 2']); $this->assertDatabaseHas('roles', ['name' => 'Role 3']); }); /* |-------------------------------------------------------------------------- | EDIT / UPDATE |-------------------------------------------------------------------------- */ test('authenticated users without permission cannot visit the edit page', function () { $user = User::factory()->create(); $this->actingAs($user); $role = Role::create(['name' => 'To Edit']); $response = $this->get(route('admin.settings.roles.edit', $role)); $response->assertForbidden(); }); test('authenticated users with permission can visit the edit page', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'To Edit']); $response = $this->get(route('admin.settings.roles.edit', $role)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/edit') ->has('role') ->has('permissions') ); }); test('role can be updated', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Old Name']); $role->syncPermissions(['user.view']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'New Name', 'permissions' => ['user.view', 'user.create'], ]); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.settings.roles.index')); $role->refresh(); expect($role->name)->toBe('New Name'); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['user.create', 'user.view']); }); test('role name can be updated to itself', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Same Name']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Same Name', 'permissions' => ['user.view'], ]); $response->assertSessionHasNoErrors(); }); test('role update name must be unique excluding itself', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'First']); Role::create(['name' => 'Second']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Second', 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role update name is required', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Existing']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => '', 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role update name must not exceed 100 characters', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Existing']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => str_repeat('a', 101), 'permissions' => ['user.view'], ]); $response->assertSessionHasErrors('name'); }); test('role permissions can be updated', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Update Perm']); $role->syncPermissions(['user.view']); $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Update Perm', 'permissions' => ['category.view', 'category.create'], ]); $role->refresh(); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.create', 'category.view']); }); test('role can be updated multiple times', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Version 1']); $this->put(route('admin.settings.roles.update', $role), ['name' => 'Version 2', 'permissions' => ['user.view']]); $role->refresh(); expect($role->name)->toBe('Version 2'); $this->put(route('admin.settings.roles.update', $role), ['name' => 'Version 3', 'permissions' => ['user.view']]); $role->refresh(); expect($role->name)->toBe('Version 3'); }); test('update flashes success toast via Inertia', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Toast']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Updated Toast', 'permissions' => ['user.view'], ]); $response->assertRedirect(); }); test('updating non-existent role returns 404', function () { $this->actingAs(createUserWithRole()); $response = $this->put(route('admin.settings.roles.update', 999999), [ 'name' => 'Ghost Role', 'permissions' => ['user.view'], ]); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | DELETE / DESTROY |-------------------------------------------------------------------------- */ test('authenticated users without permission cannot delete roles', function () { $user = User::factory()->create(); $this->actingAs($user); $role = Role::create(['name' => 'To Delete']); $response = $this->delete(route('admin.settings.roles.destroy', $role)); $response->assertForbidden(); $this->assertDatabaseHas('roles', ['id' => $role->id]); }); test('role can be deleted', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'To Delete']); $response = $this->delete(route('admin.settings.roles.destroy', $role)); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.settings.roles.index')); $this->assertDatabaseMissing('roles', ['id' => $role->id]); }); test('role permissions are removed when role is deleted', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'With Perms']); $role->syncPermissions(['user.view', 'user.create']); $this->delete(route('admin.settings.roles.destroy', $role)); $this->assertDatabaseMissing('roles', ['id' => $role->id]); $this->assertDatabaseMissing('role_has_permissions', ['role_id' => $role->id]); }); test('delete flashes success toast via Inertia', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Toast Delete']); $response = $this->delete(route('admin.settings.roles.destroy', $role)); $response->assertRedirect(); }); test('deleting non-existent role returns 404', function () { $this->actingAs(createUserWithRole()); $response = $this->delete(route('admin.settings.roles.destroy', 999999)); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS |-------------------------------------------------------------------------- */ test('guest cannot create role', function () { $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Unauthorized', 'permissions' => ['user.view'], ]); $response->assertRedirect(route('login')); $this->assertDatabaseMissing('roles', ['name' => 'Unauthorized']); }); test('guest cannot update role', function () { $role = Role::create(['name' => 'Existing']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Unauthorized Update', 'permissions' => ['user.view'], ]); $response->assertRedirect(route('login')); }); test('guest cannot delete role', function () { $role = Role::create(['name' => 'Existing']); $response = $this->delete(route('admin.settings.roles.destroy', $role)); $response->assertRedirect(route('login')); $this->assertDatabaseHas('roles', ['id' => $role->id]); }); /* |-------------------------------------------------------------------------- | AUTHORIZATION - USER WITHOUT ROLE CANNOT PERFORM ACTIONS |-------------------------------------------------------------------------- */ test('user without developer role cannot create role', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->post(route('admin.settings.roles.store'), [ 'name' => 'Unauthorized', 'permissions' => ['user.view'], ]); $response->assertForbidden(); $this->assertDatabaseMissing('roles', ['name' => 'Unauthorized']); }); test('user without developer role cannot update role', function () { $user = User::factory()->create(); $this->actingAs($user); $role = Role::create(['name' => 'Existing']); $response = $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Unauthorized Update', 'permissions' => ['user.view'], ]); $response->assertForbidden(); }); test('user without developer role cannot delete role', function () { $user = User::factory()->create(); $this->actingAs($user); $role = Role::create(['name' => 'Existing']); $response = $this->delete(route('admin.settings.roles.destroy', $role)); $response->assertForbidden(); $this->assertDatabaseHas('roles', ['id' => $role->id]); }); /* |-------------------------------------------------------------------------- | DATA INTEGRITY |-------------------------------------------------------------------------- */ test('created role has correct timestamps', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), [ 'name' => 'Timestamp Role', 'permissions' => ['user.view'], ]); $role = Role::where('name', 'Timestamp Role')->first(); expect($role->created_at)->not->toBeNull(); expect($role->updated_at)->not->toBeNull(); }); /* |-------------------------------------------------------------------------- | PERMISSION ASSIGNMENT |-------------------------------------------------------------------------- */ test('role permissions are correctly synced', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Sync Test']); $role->syncPermissions(['user.view', 'user.create', 'category.view']); $role->refresh(); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.view', 'user.create', 'user.view']); }); test('role permissions can be replaced', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Replace Test']); $role->syncPermissions(['user.view']); $role->syncPermissions(['category.view', 'category.create']); $role->refresh(); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.create', 'category.view']); }); test('role can have all permissions', function () { $this->actingAs(createUserWithRole()); $allPermissions = Permission::pluck('name')->toArray(); $role = Role::create(['name' => 'Super Admin']); $role->syncPermissions($allPermissions); $role->refresh(); expect($role->permissions->count())->toBe(count($allPermissions)); }); /* |-------------------------------------------------------------------------- | REALISTIC USER SCENARIOS |-------------------------------------------------------------------------- */ test('user creates role then immediately edits it', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), [ 'name' => 'Draft Role', 'permissions' => ['user.view'], ]); $role = Role::where('name', 'Draft Role')->first(); $this->put(route('admin.settings.roles.update', $role), [ 'name' => 'Final Role', 'permissions' => ['user.view', 'user.create'], ]); $role->refresh(); expect($role->name)->toBe('Final Role'); expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['user.create', 'user.view']); }); test('user creates multiple roles and deletes one', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), ['name' => 'Keep Role', 'permissions' => ['user.view']]); $this->post(route('admin.settings.roles.store'), ['name' => 'Delete Role', 'permissions' => ['user.view']]); $this->post(route('admin.settings.roles.store'), ['name' => 'Also Keep Role', 'permissions' => ['user.view']]); $toDelete = Role::where('name', 'Delete Role')->first(); $this->delete(route('admin.settings.roles.destroy', $toDelete)); $response = $this->get(route('admin.settings.roles.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/roles/index') ->has('roles.data', 3) ->where('roles.total', 3) ->where('roles.current_page', 1) ->where('roles.per_page', 25) ); }); test('user tries to create role without submitting any data', function () { $this->actingAs(createUserWithRole()); $response = $this->post(route('admin.settings.roles.store'), []); $response->assertSessionHasErrors(['name', 'permissions']); }); test('user tries to update role without submitting any data', function () { $this->actingAs(createUserWithRole()); $role = Role::create(['name' => 'Existing']); $response = $this->put(route('admin.settings.roles.update', $role), []); $response->assertSessionHasErrors(['name', 'permissions']); }); test('user rapidly submits same role creation twice', function () { $this->actingAs(createUserWithRole()); $this->post(route('admin.settings.roles.store'), ['name' => 'Rapid Submit', 'permissions' => ['user.view']]); $response = $this->post(route('admin.settings.roles.store'), ['name' => 'Rapid Submit', 'permissions' => ['user.view']]); $response->assertSessionHasErrors('name'); $this->assertDatabaseCount('roles', 2); });