Test Setup
Always reset the permission cache before each test to ensure clean state.use Spatie\Permission\PermissionRegistrar;
beforeEach(function () {
// Reset permission cache
app(PermissionRegistrar::class)->forgetCachedPermissions();
// Create test user and permissions
$this->user = User::create(['email' => 'test@example.com']);
$this->permission = Permission::create(['name' => 'edit-articles']);
$this->role = Role::create(['name' => 'editor']);
});
Use Pest’s
beforeEach() or PHPUnit’s setUp() to establish a consistent test environment:protected function setUp(): void
{
parent::setUp();
$this->artisan('permission:cache-reset');
}
Testing Permission Assignment
Verify permissions can be assigned and checked correctly.it('can assign a permission to a user', function () {
$user = User::create(['email' => 'test@example.com']);
$permission = Permission::create(['name' => 'edit-articles']);
$user->givePermissionTo($permission);
expect($user->hasPermissionTo('edit-articles'))->toBeTrue();
expect($user->hasDirectPermission('edit-articles'))->toBeTrue();
});
it('can revoke a permission from a user', function () {
$user = User::create(['email' => 'test@example.com']);
$permission = Permission::create(['name' => 'edit-articles']);
$user->givePermissionTo($permission);
expect($user->hasPermissionTo($permission))->toBeTrue();
$user->revokePermissionTo($permission);
expect($user->hasPermissionTo($permission))->toBeFalse();
});
Always test both positive and negative cases to ensure permissions work as expected.
Testing with Multiple Guards
Test that guard separation is properly enforced.use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
it('throws exception when assigning permission from different guard', function () {
$user = User::create(['email' => 'test@example.com']);
$adminPermission = Permission::create([
'name' => 'admin-permission',
'guard_name' => 'admin'
]);
expect(fn() => $user->givePermissionTo($adminPermission))
->toThrow(GuardDoesNotMatch::class);
});
it('can check permissions for non-default guard', function () {
$permission = Permission::create([
'name' => 'edit-articles',
'guard_name' => 'api'
]);
$user->givePermissionTo($permission);
expect($user->hasPermissionTo($permission))->toBeTrue();
});
Always create permissions with the correct guard before testing:
// ❌ This will fail - permission doesn't exist for 'web' guard
expect(fn() => $user->hasPermissionTo('admin-permission'))
->toThrow(PermissionDoesNotExist::class);
Testing Role Permissions
Test that permissions work correctly through role assignment.it('can determine if user has permission through roles', function () {
$user = User::create(['email' => 'test@example.com']);
$role = Role::create(['name' => 'editor']);
$permission = Permission::create(['name' => 'edit-articles']);
$role->givePermissionTo($permission);
$user->assignRole($role);
expect($user->hasPermissionTo($permission))->toBeTrue();
expect($user->hasDirectPermission($permission))->toBeFalse();
expect($user->hasPermissionViaRole($permission))->toBeTrue();
});
it('can list all permissions via roles', function () {
$user = User::create(['email' => 'test@example.com']);
$role1 = Role::create(['name' => 'editor']);
$role2 = Role::create(['name' => 'author']);
$role1->givePermissionTo('edit-articles');
$role2->givePermissionTo('edit-news');
$user->assignRole('editor', 'author');
expect($user->getPermissionsViaRoles()->pluck('name')->sort()->values())
->toEqual(collect(['edit-articles', 'edit-news']));
});
Testing with Laravel Gates
Verify that permissions integrate correctly with Laravel’s authorization Gate.use Illuminate\Support\Facades\Gate;
it('can determine if user does not have permission', function () {
$user = User::create(['email' => 'test@example.com']);
Permission::create(['name' => 'edit-articles']);
expect($user->can('edit-articles'))->toBeFalse();
});
it('can determine if user has direct permission', function () {
$user = User::create(['email' => 'test@example.com']);
$permission = Permission::create(['name' => 'edit-articles']);
$user->givePermissionTo($permission);
expect($user->can('edit-articles'))->toBeTrue();
expect($user->can('non-existing-permission'))->toBeFalse();
});
it('allows gate before callback to override permissions', function () {
$user = User::create(['email' => 'test@example.com']);
Permission::create(['name' => 'edit-articles']);
expect($user->can('edit-articles'))->toBeFalse();
// Super admin gate
Gate::before(function () {
return true; // Override for super admin
});
expect($user->can('edit-articles'))->toBeTrue();
});
Gate callbacks allow you to test super-admin scenarios:
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
Testing Middleware
Test route protection with permission middleware.use Spatie\Permission\Middleware\PermissionMiddleware;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
it('guest cannot access protected route', function () {
$middleware = new PermissionMiddleware();
$request = Request::create('/admin', 'GET');
expect(function () use ($middleware, $request) {
$middleware->handle($request, fn() => new Response(), 'edit-articles');
})->toThrow(UnauthorizedException::class);
});
it('user can access route with correct permission', function () {
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-articles');
Auth::login($user);
$response = $this->get('/admin/articles');
$response->assertStatus(200);
});
it('user cannot access route without permission', function () {
$user = User::create(['email' => 'test@example.com']);
Auth::login($user);
$response = $this->get('/admin/articles');
$response->assertStatus(403);
});
Testing OR Permissions in Middleware
Testing OR Permissions in Middleware
it('user can access route with one of multiple permissions', function () {
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-articles');
Auth::login($user);
// Route protected with: permission:edit-articles|edit-news
$response = $this->get('/admin/content');
$response->assertStatus(200);
});
Testing Query Scopes
Test that database query scopes filter users correctly.it('can scope users using permission', function () {
User::create(['email' => 'user1@test.com'])
->givePermissionTo(['edit-articles', 'edit-news']);
$role = Role::create(['name' => 'editor']);
$role->givePermissionTo('edit-articles');
User::create(['email' => 'user2@test.com'])
->assignRole('editor');
User::create(['email' => 'user3@test.com']);
$editors = User::permission('edit-articles')->get();
$withoutNews = User::withoutPermission('edit-news')->get();
expect($editors->count())->toEqual(2);
expect($withoutNews->count())->toEqual(2);
});
it('can scope users by multiple permissions', function () {
User::create(['email' => 'user1@test.com'])
->givePermissionTo(['edit-articles', 'edit-news']);
User::create(['email' => 'user2@test.com'])
->givePermissionTo('edit-articles');
$scopedUsers = User::permission(['edit-articles', 'edit-news'])->get();
expect($scopedUsers->count())->toEqual(2);
});
Testing Cache Behavior
Verify that cache is properly managed during permission operations.use Illuminate\Support\Facades\DB;
beforeEach(function () {
$this->registrar = app(PermissionRegistrar::class);
$this->registrar->forgetCachedPermissions();
DB::enableQueryLog();
});
function resetQueryCount(): void
{
DB::flushQueryLog();
}
function assertQueryCount(int $expected): void
{
expect(DB::getQueryLog())->toHaveCount($expected);
}
it('caches permissions after first load', function () {
resetQueryCount();
$this->registrar->getPermissions();
$initialQueries = count(DB::getQueryLog());
resetQueryCount();
$this->registrar->getPermissions();
assertQueryCount(0); // No queries - loaded from cache
});
it('flushes cache when creating permission', function () {
$this->registrar->getPermissions(); // Load cache
Permission::create(['name' => 'new-permission']);
resetQueryCount();
$this->registrar->getPermissions(); // Cache was flushed
expect(count(DB::getQueryLog()))->toBeGreaterThan(0);
});
it('does not flush cache when assigning permission to user', function () {
$user = User::create(['email' => 'test@example.com']);
$this->registrar->getPermissions(); // Load cache
$user->givePermissionTo('edit-articles');
resetQueryCount();
$this->registrar->getPermissions();
assertQueryCount(0); // Cache not flushed - optimization
});
From
CacheTest.php:98: User-level permission changes don’t flush the global cache because they don’t affect the permission structure itself. This is a performance optimization.Testing Permission Sync
Test thatsyncPermissions() works correctly and efficiently.
it('can sync multiple permissions', function () {
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-news');
$user->syncPermissions('edit-articles', 'edit-blog');
expect($user->hasDirectPermission('edit-articles'))->toBeTrue();
expect($user->hasDirectPermission('edit-blog'))->toBeTrue();
expect($user->hasDirectPermission('edit-news'))->toBeFalse();
});
it('avoids sync duplicated permissions', function () {
$user = User::create(['email' => 'test@example.com']);
$user->syncPermissions('edit-articles', 'edit-blog', 'edit-blog');
expect($user->permissions()->count())->toEqual(2);
});
it('does not detach on sync error', function () {
$user = User::create(['email' => 'test@example.com']);
$user->syncPermissions('edit-articles');
expect(fn() => $user->syncPermissions('permission-does-not-exist'))
->toThrow(PermissionDoesNotExist::class);
expect($user->fresh()->hasDirectPermission('edit-articles'))->toBeTrue();
});
Testing with Enums
Test PHP 8.1+ enum support for type-safe permissions.enum ArticlePermission: string
{
case View = 'view articles';
case Edit = 'edit articles';
case Delete = 'delete articles';
}
it('can assign and check permissions using enums', function () {
$enum = ArticlePermission::Edit;
$permission = Permission::findOrCreate($enum->value, 'web');
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo($enum);
expect($user->hasPermissionTo($enum))->toBeTrue();
expect($user->hasAnyPermission($enum))->toBeTrue();
expect($user->hasDirectPermission($enum))->toBeTrue();
});
it('can revoke permissions using enums', function () {
$enum = ArticlePermission::Edit;
Permission::findOrCreate($enum->value, 'web');
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo($enum);
$user->revokePermissionTo($enum);
expect($user->hasPermissionTo($enum))->toBeFalse();
});
Testing Events
Test that permission events fire correctly when enabled.use Illuminate\Support\Facades\Event;
use Spatie\Permission\Events\PermissionAttachedEvent;
use Spatie\Permission\Events\PermissionDetachedEvent;
it('fires event when permission is added', function () {
Event::fake();
config()->set('permission.events_enabled', true);
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo(['edit-articles', 'edit-news']);
Event::assertDispatched(PermissionAttachedEvent::class, function ($event) use ($user) {
return $event->model instanceof User
&& $event->model->id === $user->id
&& count($event->permissionsOrIds) === 2;
});
});
it('does not fire event when events are disabled', function () {
Event::fake();
config()->set('permission.events_enabled', false);
$user = User::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-articles');
Event::assertNotDispatched(PermissionAttachedEvent::class);
});
it('fires event when permission is removed', function () {
Event::fake();
config()->set('permission.events_enabled', true);
$user = User::create(['email' => 'test@example.com']);
$permissions = Permission::whereIn('name', ['edit-articles', 'edit-news'])->get();
$user->givePermissionTo($permissions);
$user->revokePermissionTo($permissions);
Event::assertDispatched(PermissionDetachedEvent::class, function ($event) use ($user) {
return $event->model instanceof User
&& !$event->model->hasPermissionTo('edit-news')
&& !$event->model->hasPermissionTo('edit-articles');
});
});
Don’t forget to enable events in your test configuration:
config()->set('permission.events_enabled', true);
Testing Soft Deletes
Verify that permissions are preserved during soft deletes.use App\Models\SoftDeletingUser;
it('does not detach permissions when user soft deletes', function () {
$user = SoftDeletingUser::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-news');
$user->delete(); // Soft delete
$user = SoftDeletingUser::withTrashed()->find($user->id);
expect($user->hasPermissionTo('edit-news'))->toBeTrue();
});
it('detaches permissions on force delete', function () {
$user = SoftDeletingUser::create(['email' => 'test@example.com']);
$user->givePermissionTo('edit-news');
$userId = $user->id;
$user->forceDelete();
// Verify pivot records are removed
$pivotRecords = DB::table('model_has_permissions')
->where('model_id', $userId)
->count();
expect($pivotRecords)->toEqual(0);
});
Factory Patterns for Testing
Create reusable factories for test permissions and roles.database/factories/PermissionFactory.php
use Spatie\Permission\Models\Permission;
class PermissionFactory extends Factory
{
protected $model = Permission::class;
public function definition(): array
{
return [
'name' => $this->faker->unique()->word,
'guard_name' => 'web',
];
}
public function forGuard(string $guard): self
{
return $this->state(['guard_name' => $guard]);
}
}
// In your tests
it('can create permissions with factory', function () {
$permission = Permission::factory()->create(['name' => 'edit-articles']);
$adminPermission = Permission::factory()->forGuard('admin')->create();
$user = User::factory()->create();
$user->givePermissionTo($permission);
expect($user->hasPermissionTo('edit-articles'))->toBeTrue();
});
Create test helpers for common permission scenarios:
tests/Helpers/PermissionHelpers.php
function createUserWithPermissions(array $permissions): User
{
$user = User::factory()->create();
$user->givePermissionTo($permissions);
return $user;
}
function createUserWithRole(string $roleName, array $permissions = []): User
{
$role = Role::create(['name' => $roleName]);
$role->givePermissionTo($permissions);
$user = User::factory()->create();
$user->assignRole($role);
return $user;
}