laravel-roles/tests/Unit/HasPermissionsTest.php

469 lines
16 KiB
PHP

<?php
namespace Blax\Roles\Tests\Unit;
use Blax\Roles\Models\Permission;
use Blax\Roles\Models\Role;
use Blax\Roles\RolesServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Orchestra\Testbench\TestCase;
use Workbench\App\Models\User;
class HasPermissionsTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [RolesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$app['config']->set('roles.run_migrations', false);
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
// ─── hasPermission ───────────────────────────────────────────
public function test_has_permission_returns_false_when_user_has_no_permissions(): void
{
$user = User::factory()->create();
$this->assertFalse($user->hasPermission('blog'));
}
public function test_has_permission_with_direct_exact_match(): void
{
$user = User::factory()->create();
$user->assignPermission('blog');
$this->assertTrue($user->hasPermission('blog'));
}
public function test_has_permission_with_hierarchical_parent_grants_child(): void
{
$user = User::factory()->create();
$user->assignPermission('lection');
$this->assertTrue($user->hasPermission('lection'));
$this->assertTrue($user->hasPermission('lection.45'));
$this->assertTrue($user->hasPermission('lection.foo.bar'));
}
public function test_has_permission_hierarchical_child_does_not_grant_parent(): void
{
$user = User::factory()->create();
$user->assignPermission('lection.45');
$this->assertTrue($user->hasPermission('lection.45'));
$this->assertFalse($user->hasPermission('lection'));
}
public function test_has_permission_hierarchical_sibling_not_granted(): void
{
$user = User::factory()->create();
$user->assignPermission('lection.45');
$this->assertFalse($user->hasPermission('lection.99'));
}
public function test_has_permission_wildcard_star_grants_everything(): void
{
$user = User::factory()->create();
$user->assignPermission('*');
$this->assertTrue($user->hasPermission('anything'));
$this->assertTrue($user->hasPermission('blog.edit'));
$this->assertTrue($user->hasPermission('deep.nested.permission.chain'));
}
public function test_has_permission_via_role(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Editor', 'slug' => 'editor']);
$perm = Permission::create(['slug' => 'blog.edit']);
// Assign permission to role
$role->assignPermission($perm);
// Assign role to user
$user->assignRole($role);
$this->assertTrue($user->hasPermission('blog.edit'));
}
public function test_has_permission_via_role_hierarchical(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Learner', 'slug' => 'learner']);
$perm = Permission::create(['slug' => 'lection']);
$role->assignPermission($perm);
$user->assignRole($role);
$this->assertTrue($user->hasPermission('lection'));
$this->assertTrue($user->hasPermission('lection.45'));
$this->assertTrue($user->hasPermission('lection.foo.bar'));
$this->assertFalse($user->hasPermission('blog'));
}
public function test_has_permission_does_not_match_partial_slug(): void
{
$user = User::factory()->create();
$user->assignPermission('blog');
// "blogging" is NOT a child of "blog" — it doesn't start with "blog."
$this->assertFalse($user->hasPermission('blogging'));
}
// ─── rolePermissions ─────────────────────────────────────────
public function test_role_permissions_returns_empty_when_no_roles(): void
{
$user = User::factory()->create();
$this->assertCount(0, $user->rolePermissions());
}
public function test_role_permissions_returns_permissions_from_assigned_role(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Admin', 'slug' => 'admin']);
$perm1 = Permission::create(['slug' => 'blog.edit']);
$perm2 = Permission::create(['slug' => 'blog.delete']);
$role->assignPermission($perm1);
$role->assignPermission($perm2);
$user->assignRole($role);
$perms = $user->rolePermissions();
$this->assertCount(2, $perms);
$this->assertTrue($perms->contains('slug', 'blog.edit'));
$this->assertTrue($perms->contains('slug', 'blog.delete'));
}
public function test_role_permissions_deduplicates_across_multiple_roles(): void
{
$user = User::factory()->create();
$role1 = Role::create(['name' => 'Editor', 'slug' => 'editor']);
$role2 = Role::create(['name' => 'Reviewer', 'slug' => 'reviewer']);
$perm = Permission::create(['slug' => 'blog.view']);
$role1->assignPermission($perm);
$role2->assignPermission($perm);
$user->assignRole($role1);
$user->assignRole($role2);
// rolePermissions returns raw — but permissions() deduplicates
$perms = $user->permissions();
$this->assertCount(1, $perms);
}
public function test_role_permissions_excludes_expired_role_membership(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Temp', 'slug' => 'temp']);
$perm = Permission::create(['slug' => 'temp.access']);
$role->assignPermission($perm);
// Manually insert an expired role membership
DB::table(config('roles.table_names.role_member'))->insert([
'role_id' => $role->id,
'member_id' => $user->id,
'member_type' => $user->getMorphClass(),
'expires_at' => now()->subDay(),
'created_at' => now(),
'updated_at' => now(),
]);
$this->assertCount(0, $user->rolePermissions());
}
public function test_role_permissions_includes_non_expired_role_membership(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Active', 'slug' => 'active']);
$perm = Permission::create(['slug' => 'active.access']);
$role->assignPermission($perm);
// Manually insert a future-expiry role membership
DB::table(config('roles.table_names.role_member'))->insert([
'role_id' => $role->id,
'member_id' => $user->id,
'member_type' => $user->getMorphClass(),
'expires_at' => now()->addDays(7),
'created_at' => now(),
'updated_at' => now(),
]);
$this->assertCount(1, $user->rolePermissions());
}
public function test_role_permissions_includes_null_expiry_role_membership(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Permanent', 'slug' => 'permanent']);
$perm = Permission::create(['slug' => 'permanent.access']);
$role->assignPermission($perm);
$user->assignRole($role);
$this->assertCount(1, $user->rolePermissions());
}
public function test_role_permissions_excludes_expired_permission_on_role(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Editor', 'slug' => 'editor']);
$perm = Permission::create(['slug' => 'temp.perm']);
// Manually attach permission to role with expired membership
DB::table(config('roles.table_names.permission_member'))->insert([
'permission_id' => $perm->id,
'member_id' => $role->id,
'member_type' => $role->getMorphClass(),
'expires_at' => now()->subHour(),
'created_at' => now(),
'updated_at' => now(),
]);
$user->assignRole($role);
$this->assertCount(0, $user->rolePermissions());
}
// ─── individualPermissions ───────────────────────────────────
public function test_individual_permissions_returns_empty_when_none_assigned(): void
{
$user = User::factory()->create();
$this->assertCount(0, $user->individualPermissions()->get());
}
public function test_individual_permissions_returns_directly_assigned(): void
{
$user = User::factory()->create();
$user->assignPermission('direct.perm');
$perms = $user->individualPermissions()->get();
$this->assertCount(1, $perms);
$this->assertEquals('direct.perm', $perms->first()->slug);
}
// ─── permissions (merged) ────────────────────────────────────
public function test_permissions_merges_role_and_direct_permissions(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Writer', 'slug' => 'writer']);
$rolePerm = Permission::create(['slug' => 'blog.write']);
$role->assignPermission($rolePerm);
$user->assignRole($role);
$user->assignPermission('profile.edit');
$all = $user->permissions();
$this->assertCount(2, $all);
$this->assertTrue($all->contains('slug', 'blog.write'));
$this->assertTrue($all->contains('slug', 'profile.edit'));
}
public function test_permissions_deduplicates_when_same_permission_from_role_and_direct(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Writer', 'slug' => 'writer']);
$perm = Permission::create(['slug' => 'shared.perm']);
$role->assignPermission($perm);
$user->assignRole($role);
$user->assignPermission('shared.perm');
$all = $user->permissions();
$this->assertCount(1, $all);
}
// ─── assignPermission ────────────────────────────────────────
public function test_assign_permission_by_slug_string(): void
{
$user = User::factory()->create();
$user->assignPermission('new.permission');
$this->assertTrue($user->hasPermission('new.permission'));
// Should have created the permission
$this->assertDatabaseHas('permissions', ['slug' => 'new.permission']);
}
public function test_assign_permission_by_id(): void
{
$perm = Permission::create(['slug' => 'by.id']);
$user = User::factory()->create();
$user->assignPermission($perm->id);
$this->assertTrue($user->hasPermission('by.id'));
}
public function test_assign_permission_by_model_instance(): void
{
$perm = Permission::create(['slug' => 'by.instance']);
$user = User::factory()->create();
$user->assignPermission($perm);
$this->assertTrue($user->hasPermission('by.instance'));
}
public function test_assign_permission_is_idempotent(): void
{
$user = User::factory()->create();
$user->assignPermission('idempotent.test');
$user->assignPermission('idempotent.test');
// Should only have one entry in the pivot table
$count = DB::table(config('roles.table_names.permission_member'))
->where('member_id', $user->id)
->where('member_type', $user->getMorphClass())
->count();
$this->assertEquals(1, $count);
}
public function test_assign_permission_throws_on_invalid_argument(): void
{
$user = User::factory()->create();
$this->expectException(\InvalidArgumentException::class);
$user->assignPermission(['invalid']);
}
public function test_assign_permission_creates_permission_if_not_exists(): void
{
$user = User::factory()->create();
$this->assertDatabaseMissing('permissions', ['slug' => 'auto.created']);
$user->assignPermission('auto.created');
$this->assertDatabaseHas('permissions', ['slug' => 'auto.created']);
}
// ─── removePermission ────────────────────────────────────────
public function test_remove_permission_by_slug(): void
{
$user = User::factory()->create();
$user->assignPermission('to.remove');
$this->assertTrue($user->hasPermission('to.remove'));
$user->removePermission('to.remove');
// Reload permissions
$this->assertFalse($user->hasPermission('to.remove'));
}
public function test_remove_permission_by_id(): void
{
$perm = Permission::create(['slug' => 'remove.by.id']);
$user = User::factory()->create();
$user->assignPermission($perm);
$user->removePermission($perm->id);
$count = $user->individualPermissions()->count();
$this->assertEquals(0, $count);
}
public function test_remove_permission_by_model(): void
{
$perm = Permission::create(['slug' => 'remove.by.model']);
$user = User::factory()->create();
$user->assignPermission($perm);
$user->removePermission($perm);
$count = $user->individualPermissions()->count();
$this->assertEquals(0, $count);
}
public function test_remove_permission_that_does_not_exist_returns_true(): void
{
$user = User::factory()->create();
$result = $user->removePermission('nonexistent');
$this->assertTrue($result);
}
public function test_remove_permission_does_not_affect_other_permissions(): void
{
$user = User::factory()->create();
$user->assignPermission('keep.this');
$user->assignPermission('remove.this');
$user->removePermission('remove.this');
$this->assertTrue($user->hasPermission('keep.this'));
$this->assertFalse($user->hasPermission('remove.this'));
}
public function test_remove_permission_does_not_affect_role_permissions(): void
{
$user = User::factory()->create();
$role = Role::create(['name' => 'Admin', 'slug' => 'admin']);
$perm = Permission::create(['slug' => 'role.perm']);
$role->assignPermission($perm);
$user->assignRole($role);
// User also directly has the same permission
$user->assignPermission($perm);
// Remove the direct assignment
$user->removePermission($perm);
// The user should still have it via role
$this->assertTrue($user->hasPermission('role.perm'));
}
// ─── Role model also uses HasPermissions ─────────────────────
public function test_role_can_have_permissions_assigned(): void
{
$role = Role::create(['name' => 'Moderator', 'slug' => 'moderator']);
$role->assignPermission('moderate.posts');
$this->assertTrue($role->hasPermission('moderate.posts'));
}
public function test_role_has_permission_hierarchical(): void
{
$role = Role::create(['name' => 'Learner', 'slug' => 'learner']);
$role->assignPermission('lection');
$this->assertTrue($role->hasPermission('lection.42'));
$this->assertFalse($role->hasPermission('blog'));
}
// ─── Multiple users independence ─────────────────────────────
public function test_permissions_are_independent_between_users(): void
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user1->assignPermission('user1.only');
$user2->assignPermission('user2.only');
$this->assertTrue($user1->hasPermission('user1.only'));
$this->assertFalse($user1->hasPermission('user2.only'));
$this->assertTrue($user2->hasPermission('user2.only'));
$this->assertFalse($user2->hasPermission('user1.only'));
}
}