Permissions::fake() swaps in a recording PermissionsFake and returns it. Everything still runs against the database — grants land and the Gate answers — while every write is recorded, whether it came through the facade, an injected manager, a for() handle, the traits, Role::findOrCreate() / Permission::findOrCreate() or the artisan commands. Names are normalized, so an enum matches its string:
use RoundlyConsulting\Permissions\Facades\Permissions;
$fake = Permissions::fake();
$user->assignRole('editor');
Permissions::syncFrom(PostPermission::class);
$fake->assertRoleAssigned($user, RoleName::Editor);
$fake->assertSyncedFrom(PostPermission::class);
$fake->assertNoPermissionGranted();Because the fake is also bound in the container, a class that injects PermissionsManager is covered too:
use RoundlyConsulting\Permissions\Facades\Permissions;
it('makes new staff editors', function () {
$fake = Permissions::fake();
$user = User::factory()->create();
app(OnboardEditor::class)->handle($user); // injects PermissionsManager
$fake->assertRoleRegistered('editor');
$fake->assertRoleAssigned($user, 'editor');
expect($user->hasRole('editor'))->toBeTrue(); // still performed — the row exists
});Every assertion
| Assertion | Negative | Recorded from |
|---|---|---|
assertRoleRegistered($name) | assertNoRoleRegistered() | role(), Role::findOrCreate() |
assertPermissionRegistered($name) | assertNoPermissionRegistered() | permission(), Permission::findOrCreate() |
assertSyncedFrom($enum) | assertNothingSyncedFrom() | syncFrom(), syncRolesFrom() |
assertRoleAssigned($holder, ?$role) | assertNoRoleAssigned() | assignRole() |
assertRoleRemoved($holder, ?$role) | assertNoRoleRemoved() | removeRole() |
assertRolesSynced($holder, ?$roles) | assertNoRolesSynced() | syncRoles() — exact set |
assertPermissionGranted($holder, ?$permission) | assertNoPermissionGranted() | givePermissionTo() |
assertPermissionRevoked($holder, ?$permission) | assertNoPermissionRevoked() | revokePermissionTo() |
assertPermissionsSynced($holder, ?$permissions) | assertNoPermissionsSynced() | syncPermissions() — exact set |
assertAuthorizationForgotten($holder) | assertNoAuthorizationForgotten() | forgetAllAuthorization() |
assertOrphansPruned() | assertNoOrphansPruned() | pruneOrphans(), permissions:prune-orphans |
assertCacheForgotten() | assertCacheNotForgotten() | cache()->forget(), permissions:cache-reset |
assertMemoFlushed() | assertMemoNotFlushed() | cache()->flushMemo() |
The holder-scoped assertions take the role or permission as an optional second argument; the synced ones compare the exact set. Every negative takes no arguments:
// Catalog
$fake->assertRoleRegistered('editor'); // role(), Role::findOrCreate()
$fake->assertPermissionRegistered('posts.edit'); // permission(), Permission::findOrCreate()
$fake->assertSyncedFrom(PostPermission::class); // syncFrom(), syncRolesFrom()
// Roles on a holder — the role argument is optional
$fake->assertRoleAssigned($user); // any role
$fake->assertRoleAssigned($user, RoleName::Editor); // this role
$fake->assertRoleRemoved($user, 'administrator');
$fake->assertRolesSynced($user, ['editor']); // exactly this set
// Direct permissions on a holder or a role
$fake->assertPermissionGranted($role, 'posts.edit');
$fake->assertPermissionRevoked($role, 'posts.edit');
$fake->assertPermissionsSynced($user, [PostPermission::Edit]); // exactly this set
// Cleanup and cache
$fake->assertAuthorizationForgotten($user);
$fake->assertOrphansPruned();
$fake->assertCacheForgotten();
$fake->assertMemoFlushed();
// The negatives take no arguments
$fake->assertNoRoleRegistered();
$fake->assertNoPermissionRegistered();
$fake->assertNothingSyncedFrom();
$fake->assertNoRoleAssigned();
$fake->assertNoRoleRemoved();
$fake->assertNoRolesSynced();
$fake->assertNoPermissionGranted();
$fake->assertNoPermissionRevoked();
$fake->assertNoPermissionsSynced();
$fake->assertNoAuthorizationForgotten();
$fake->assertNoOrphansPruned();
$fake->assertCacheNotForgotten();
$fake->assertMemoNotFlushed();A refused write — an unknown name or the wrong holder — is not recorded. The package’s own cache housekeeping is not recorded either: the invalidation after every grant and the memo reset per job or request. The cache assertions see only explicit Permissions::cache() calls and permissions:cache-reset.
End-to-end tests
Without the fake, test against a real database with RefreshDatabase — the migrations you published run like any other. Grant through the facade, then assert through the model or through HTTP:
use RoundlyConsulting\Permissions\Facades\Permissions;
it('lets editors view users', function () {
Permissions::permission('auth.users.view');
Permissions::for(Permissions::role('editor'))->givePermissionTo('auth.users.view');
$user = User::factory()->create();
Permissions::for($user)->assignRole('editor');
expect($user->hasPermissionTo('auth.users.view'))->toBeTrue()
->and($user->can('auth.users.view'))->toBeTrue();
$this->actingAs($user)->get('/users')->assertOk();
});
it('forbids users without the permission', function () {
Permissions::permission('auth.users.view');
$this->actingAs(User::factory()->create())
->get('/users')
->assertForbidden();
});Factories
Role and Permission ship with factories; names default to a unique role-… or permission-… slug:
use RoundlyConsulting\Permissions\Facades\Permissions;
use RoundlyConsulting\Permissions\Models\Permission;
use RoundlyConsulting\Permissions\Models\Role;
$role = Role::factory()->create(); // name: "role-…"
$permission = Permission::factory()->create(['name' => 'reports.export']);
Permissions::for($role)->givePermissionTo($permission);Cache in tests
Every Eloquent write invalidates the catalog, so tests need no manual flushing. If a test seeds permissions with DB::table() or insert(), call Permissions::cache()->forget() afterwards.
The package’s own suite
To run the package’s test suite from a checkout:
composer testShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.