A ccess
This commit is contained in:
parent
7878069c0b
commit
2d3f5ec00e
|
|
@ -54,7 +54,11 @@
|
|||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"autoload-dev": {
|
||||
"psr-4": {}
|
||||
"psr-4": {
|
||||
"Blax\\Roles\\Tests\\": "tests",
|
||||
"Workbench\\App\\": "workbench/app/",
|
||||
"Workbench\\Database\\Factories\\": "workbench/database/factories/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
|
@ -99,12 +99,14 @@ trait HasRoles
|
|||
$role = config('roles.models.role', \Blax\Roles\Models\Role::class)::where('slug', $role)->first();
|
||||
} elseif (is_numeric($role)) {
|
||||
$role = config('roles.models.role', \Blax\Roles\Models\Role::class)::find($role);
|
||||
} elseif ($role instanceof Role) {
|
||||
$this->roles()->detach($role);
|
||||
} else {
|
||||
} elseif (!$role instanceof Role) {
|
||||
throw new \InvalidArgumentException('Role must be a string, numeric ID, or an instance of Role.');
|
||||
}
|
||||
|
||||
if ($role) {
|
||||
$this->roles()->detach($role);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,644 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Roles\Tests\Unit;
|
||||
|
||||
use Blax\Roles\Models\Access;
|
||||
use Blax\Roles\Models\Permission;
|
||||
use Blax\Roles\Models\Role;
|
||||
use Blax\Roles\RolesServiceProvider;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use Workbench\App\Models\Article;
|
||||
use Workbench\App\Models\User;
|
||||
|
||||
class HasAccessTest 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' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function defineDatabaseMigrations(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
|
||||
}
|
||||
|
||||
// ─── accesses relationship ───────────────────────────────────
|
||||
|
||||
public function test_accesses_returns_empty_by_default(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->assertCount(0, $user->accesses);
|
||||
}
|
||||
|
||||
// ─── grantAccess ─────────────────────────────────────────────
|
||||
|
||||
public function test_grant_access_creates_access_entry(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Test Article']);
|
||||
|
||||
$access = $user->grantAccess($article);
|
||||
|
||||
$this->assertInstanceOf(Access::class, $access);
|
||||
$this->assertEquals($user->getMorphClass(), $access->entity_type);
|
||||
$this->assertEquals($user->id, $access->entity_id);
|
||||
$this->assertEquals($article->getMorphClass(), $access->accessible_type);
|
||||
$this->assertEquals($article->id, $access->accessible_id);
|
||||
}
|
||||
|
||||
public function test_grant_access_with_context(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Contextual']);
|
||||
|
||||
$access = $user->grantAccess($article, ['reason' => 'purchased']);
|
||||
|
||||
$this->assertEquals(['reason' => 'purchased'], $access->context);
|
||||
}
|
||||
|
||||
public function test_grant_access_with_expiration(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Expiring']);
|
||||
$expiresAt = Carbon::now()->addDays(30);
|
||||
|
||||
$access = $user->grantAccess($article, null, $expiresAt);
|
||||
|
||||
$this->assertNotNull($access->expires_at);
|
||||
// Compare with second precision to avoid microsecond drift
|
||||
$this->assertEquals(
|
||||
$expiresAt->format('Y-m-d H:i:s'),
|
||||
$access->expires_at->format('Y-m-d H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_grant_access_is_idempotent(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Idempotent']);
|
||||
|
||||
$access1 = $user->grantAccess($article);
|
||||
$access2 = $user->grantAccess($article);
|
||||
|
||||
$this->assertEquals($access1->id, $access2->id);
|
||||
$this->assertEquals(1, $user->accesses()->count());
|
||||
}
|
||||
|
||||
// ─── hasAccess ───────────────────────────────────────────────
|
||||
|
||||
public function test_has_access_returns_false_when_no_access(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Locked']);
|
||||
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_returns_true_with_direct_access(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Unlocked']);
|
||||
|
||||
$user->grantAccess($article);
|
||||
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_with_class_name_and_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'ByClassName']);
|
||||
|
||||
$user->grantAccess($article);
|
||||
|
||||
$this->assertTrue($user->hasAccess(Article::class, $article->id));
|
||||
}
|
||||
|
||||
public function test_has_access_with_class_name_without_id_throws(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$user->hasAccess(Article::class);
|
||||
}
|
||||
|
||||
public function test_has_access_via_role(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Premium', 'slug' => 'premium']);
|
||||
$article = Article::create(['title' => 'Premium Article']);
|
||||
|
||||
// Grant access to the role (Role uses HasPermissions which uses HasAccess)
|
||||
$role->grantAccess($article);
|
||||
// Assign role to user
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_via_permission(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$perm = Permission::create(['slug' => 'blog.premium']);
|
||||
$article = Article::create(['title' => 'Permission Article']);
|
||||
|
||||
// Grant access to the permission (Permission uses HasAccess)
|
||||
$perm->grantAccess($article);
|
||||
// Assign permission to user
|
||||
$user->assignPermission($perm);
|
||||
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_via_role_permission_chain(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Learner', 'slug' => 'learner']);
|
||||
$perm = Permission::create(['slug' => 'lection']);
|
||||
$article = Article::create(['title' => 'Lesson']);
|
||||
|
||||
// Permission has access to article
|
||||
$perm->grantAccess($article);
|
||||
// Role has permission
|
||||
$role->assignPermission($perm);
|
||||
// User has role
|
||||
$user->assignRole($role);
|
||||
|
||||
// User should have access via: user → role → permission → access
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_expired_direct_access_returns_false(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Expired']);
|
||||
|
||||
$user->grantAccess($article, null, Carbon::now()->subDay());
|
||||
|
||||
// grantAccess uses firstOrCreate, so it won't overwrite.
|
||||
// We need to update the entry directly.
|
||||
$access = $user->accesses()->first();
|
||||
$access->update(['expires_at' => Carbon::now()->subDay()]);
|
||||
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_non_expired_returns_true(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'NotExpired']);
|
||||
|
||||
$user->grantAccess($article, null, Carbon::now()->addWeek());
|
||||
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_null_expiry_returns_true(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Permanent']);
|
||||
|
||||
$user->grantAccess($article);
|
||||
|
||||
$this->assertTrue($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_has_access_via_expired_role_returns_false(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'ExpRole', 'slug' => 'exprole']);
|
||||
$article = Article::create(['title' => 'RoleExpired']);
|
||||
|
||||
$role->grantAccess($article);
|
||||
|
||||
// Manually insert 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->assertFalse($user->hasAccess($article));
|
||||
}
|
||||
|
||||
// ─── revokeAccess ────────────────────────────────────────────
|
||||
|
||||
public function test_revoke_access_by_model(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Revoke']);
|
||||
$user->grantAccess($article);
|
||||
|
||||
$deleted = $user->revokeAccess($article);
|
||||
|
||||
$this->assertEquals(1, $deleted);
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_revoke_access_by_class_name_and_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'RevokeByClass']);
|
||||
$user->grantAccess($article);
|
||||
|
||||
$deleted = $user->revokeAccess(Article::class, $article->id);
|
||||
|
||||
$this->assertEquals(1, $deleted);
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
}
|
||||
|
||||
public function test_revoke_access_returns_zero_when_nothing_to_revoke(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'NothingToRevoke']);
|
||||
|
||||
$deleted = $user->revokeAccess($article);
|
||||
|
||||
$this->assertEquals(0, $deleted);
|
||||
}
|
||||
|
||||
public function test_revoke_access_does_not_affect_other_accessibles(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article1 = Article::create(['title' => 'Keep']);
|
||||
$article2 = Article::create(['title' => 'Remove']);
|
||||
|
||||
$user->grantAccess($article1);
|
||||
$user->grantAccess($article2);
|
||||
|
||||
$user->revokeAccess($article2);
|
||||
|
||||
$this->assertTrue($user->hasAccess($article1));
|
||||
$this->assertFalse($user->hasAccess($article2));
|
||||
}
|
||||
|
||||
// ─── revokeAllAccess ─────────────────────────────────────────
|
||||
|
||||
public function test_revoke_all_access_removes_everything(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article1 = Article::create(['title' => 'A1']);
|
||||
$article2 = Article::create(['title' => 'A2']);
|
||||
|
||||
$user->grantAccess($article1);
|
||||
$user->grantAccess($article2);
|
||||
|
||||
$deleted = $user->revokeAllAccess();
|
||||
|
||||
$this->assertEquals(2, $deleted);
|
||||
$this->assertFalse($user->hasAccess($article1));
|
||||
$this->assertFalse($user->hasAccess($article2));
|
||||
}
|
||||
|
||||
public function test_revoke_all_access_filtered_by_type(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'FilteredArticle']);
|
||||
|
||||
// Grant access to user model (different type) + article
|
||||
$otherUser = User::factory()->create();
|
||||
$user->grantAccess($article);
|
||||
$user->grantAccess($otherUser);
|
||||
|
||||
$deleted = $user->revokeAllAccess(Article::class);
|
||||
|
||||
$this->assertEquals(1, $deleted);
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
$this->assertTrue($user->hasAccess($otherUser));
|
||||
}
|
||||
|
||||
// ─── allAccess ───────────────────────────────────────────────
|
||||
|
||||
public function test_all_access_returns_direct_accesses(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article1 = Article::create(['title' => 'AA1']);
|
||||
$article2 = Article::create(['title' => 'AA2']);
|
||||
|
||||
$user->grantAccess($article1);
|
||||
$user->grantAccess($article2);
|
||||
|
||||
$accesses = $user->allAccess();
|
||||
$this->assertCount(2, $accesses);
|
||||
}
|
||||
|
||||
public function test_all_access_includes_role_based_accesses(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Reader', 'slug' => 'reader']);
|
||||
$article = Article::create(['title' => 'RoleAccess']);
|
||||
|
||||
$role->grantAccess($article);
|
||||
$user->assignRole($role);
|
||||
|
||||
$accesses = $user->allAccess();
|
||||
$this->assertCount(1, $accesses);
|
||||
$this->assertEquals($article->id, $accesses->first()->accessible_id);
|
||||
}
|
||||
|
||||
public function test_all_access_includes_permission_based_accesses(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$perm = Permission::create(['slug' => 'premium.content']);
|
||||
$article = Article::create(['title' => 'PermAccess']);
|
||||
|
||||
$perm->grantAccess($article);
|
||||
$user->assignPermission($perm);
|
||||
|
||||
$accesses = $user->allAccess();
|
||||
$this->assertCount(1, $accesses);
|
||||
}
|
||||
|
||||
public function test_all_access_filtered_by_type(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'TypeFilter']);
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
$user->grantAccess($article);
|
||||
$user->grantAccess($otherUser);
|
||||
|
||||
$articleAccesses = $user->allAccess(Article::class);
|
||||
$this->assertCount(1, $articleAccesses);
|
||||
}
|
||||
|
||||
public function test_all_access_excludes_expired_entries(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'ExpAccess']);
|
||||
|
||||
$user->grantAccess($article);
|
||||
// Manually expire
|
||||
$user->accesses()->update(['expires_at' => now()->subHour()]);
|
||||
|
||||
$accesses = $user->allAccess();
|
||||
$this->assertCount(0, $accesses);
|
||||
}
|
||||
|
||||
// ─── accessibleIds ───────────────────────────────────────────
|
||||
|
||||
public function test_accessible_ids_returns_correct_ids(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$a1 = Article::create(['title' => 'AI1']);
|
||||
$a2 = Article::create(['title' => 'AI2']);
|
||||
$a3 = Article::create(['title' => 'AI3']);
|
||||
|
||||
$user->grantAccess($a1);
|
||||
$user->grantAccess($a3);
|
||||
|
||||
$ids = $user->accessibleIds(Article::class);
|
||||
$this->assertCount(2, $ids);
|
||||
$this->assertTrue($ids->contains($a1->id));
|
||||
$this->assertTrue($ids->contains($a3->id));
|
||||
$this->assertFalse($ids->contains($a2->id));
|
||||
}
|
||||
|
||||
public function test_accessible_ids_includes_role_based(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Sub', 'slug' => 'sub']);
|
||||
$article = Article::create(['title' => 'RoleAI']);
|
||||
|
||||
$role->grantAccess($article);
|
||||
$user->assignRole($role);
|
||||
|
||||
$ids = $user->accessibleIds(Article::class);
|
||||
$this->assertCount(1, $ids);
|
||||
$this->assertTrue($ids->contains($article->id));
|
||||
}
|
||||
|
||||
public function test_accessible_ids_returns_empty_when_no_access(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
Article::create(['title' => 'NoAccess']);
|
||||
|
||||
$ids = $user->accessibleIds(Article::class);
|
||||
$this->assertCount(0, $ids);
|
||||
}
|
||||
|
||||
// ─── syncAccess ──────────────────────────────────────────────
|
||||
|
||||
public function test_sync_access_adds_and_removes_entries(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$a1 = Article::create(['title' => 'S1']);
|
||||
$a2 = Article::create(['title' => 'S2']);
|
||||
$a3 = Article::create(['title' => 'S3']);
|
||||
|
||||
// Initial: access to a1 and a2
|
||||
$user->grantAccess($a1);
|
||||
$user->grantAccess($a2);
|
||||
|
||||
// Sync to a2 and a3
|
||||
$user->syncAccess(Article::class, [$a2->id, $a3->id]);
|
||||
|
||||
$this->assertFalse($user->hasAccess($a1));
|
||||
$this->assertTrue($user->hasAccess($a2));
|
||||
$this->assertTrue($user->hasAccess($a3));
|
||||
}
|
||||
|
||||
public function test_sync_access_empty_removes_all_of_that_type(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$a1 = Article::create(['title' => 'SE1']);
|
||||
$a2 = Article::create(['title' => 'SE2']);
|
||||
|
||||
$user->grantAccess($a1);
|
||||
$user->grantAccess($a2);
|
||||
|
||||
$user->syncAccess(Article::class, []);
|
||||
|
||||
$this->assertFalse($user->hasAccess($a1));
|
||||
$this->assertFalse($user->hasAccess($a2));
|
||||
}
|
||||
|
||||
public function test_sync_access_does_not_affect_other_types(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'TypeKeep']);
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
$user->grantAccess($article);
|
||||
$user->grantAccess($otherUser);
|
||||
|
||||
// Sync articles to empty — should keep user access
|
||||
$user->syncAccess(Article::class, []);
|
||||
|
||||
$this->assertFalse($user->hasAccess($article));
|
||||
$this->assertTrue($user->hasAccess($otherUser));
|
||||
}
|
||||
|
||||
public function test_sync_access_with_context_and_expiry(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'CtxSync']);
|
||||
$expiresAt = Carbon::now()->addMonth();
|
||||
|
||||
$user->syncAccess(Article::class, [$article->id], ['reason' => 'promo'], $expiresAt);
|
||||
|
||||
$access = $user->accesses()->first();
|
||||
$this->assertEquals(['reason' => 'promo'], $access->context);
|
||||
$this->assertEquals(
|
||||
$expiresAt->format('Y-m-d H:i:s'),
|
||||
$access->expires_at->format('Y-m-d H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sync_access_preserves_existing_entries_in_new_set(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'PreserveSync']);
|
||||
|
||||
$user->grantAccess($article, ['reason' => 'original']);
|
||||
$originalId = $user->accesses()->first()->id;
|
||||
|
||||
// Sync with the same ID — should NOT recreate the entry
|
||||
$user->syncAccess(Article::class, [$article->id]);
|
||||
|
||||
$currentId = $user->accesses()->first()->id;
|
||||
$this->assertEquals($originalId, $currentId);
|
||||
}
|
||||
|
||||
// ─── Access independence ─────────────────────────────────────
|
||||
|
||||
public function test_access_is_independent_between_users(): void
|
||||
{
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
$article = Article::create(['title' => 'Independent']);
|
||||
|
||||
$user1->grantAccess($article);
|
||||
|
||||
$this->assertTrue($user1->hasAccess($article));
|
||||
$this->assertFalse($user2->hasAccess($article));
|
||||
}
|
||||
|
||||
// ─── Access scopes on model ──────────────────────────────────
|
||||
|
||||
public function test_access_active_scope(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article1 = Article::create(['title' => 'Active']);
|
||||
$article2 = Article::create(['title' => 'Expired']);
|
||||
|
||||
$user->grantAccess($article1); // no expiry = active
|
||||
$user->grantAccess($article2, null, Carbon::now()->subDay());
|
||||
// Manually expire
|
||||
$user->accesses()->where('accessible_id', $article2->id)->update(['expires_at' => now()->subDay()]);
|
||||
|
||||
$activeAccesses = Access::active()->where('entity_id', $user->id)->get();
|
||||
$this->assertCount(1, $activeAccesses);
|
||||
}
|
||||
|
||||
public function test_access_expired_scope(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'ExpiredScope']);
|
||||
|
||||
$user->grantAccess($article);
|
||||
$user->accesses()->update(['expires_at' => now()->subHour()]);
|
||||
|
||||
$expiredAccesses = Access::expired()->where('entity_id', $user->id)->get();
|
||||
$this->assertCount(1, $expiredAccesses);
|
||||
}
|
||||
|
||||
// ─── Complex multi-source access scenarios ───────────────────
|
||||
|
||||
public function test_has_access_combines_direct_and_role_sources(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Sub', 'slug' => 'sub']);
|
||||
$directArticle = Article::create(['title' => 'Direct']);
|
||||
$roleArticle = Article::create(['title' => 'ViaRole']);
|
||||
|
||||
$user->grantAccess($directArticle);
|
||||
$role->grantAccess($roleArticle);
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->assertTrue($user->hasAccess($directArticle));
|
||||
$this->assertTrue($user->hasAccess($roleArticle));
|
||||
}
|
||||
|
||||
public function test_has_access_combines_all_three_sources(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Sub', 'slug' => 'sub']);
|
||||
$perm = Permission::create(['slug' => 'premium']);
|
||||
$a1 = Article::create(['title' => 'DirectAll']);
|
||||
$a2 = Article::create(['title' => 'RoleAll']);
|
||||
$a3 = Article::create(['title' => 'PermAll']);
|
||||
|
||||
$user->grantAccess($a1);
|
||||
$role->grantAccess($a2);
|
||||
$perm->grantAccess($a3);
|
||||
|
||||
$user->assignRole($role);
|
||||
$user->assignPermission($perm);
|
||||
|
||||
$this->assertTrue($user->hasAccess($a1));
|
||||
$this->assertTrue($user->hasAccess($a2));
|
||||
$this->assertTrue($user->hasAccess($a3));
|
||||
|
||||
$allAccess = $user->allAccess(Article::class);
|
||||
$this->assertCount(3, $allAccess);
|
||||
}
|
||||
|
||||
public function test_model_without_roles_has_no_role_access(): void
|
||||
{
|
||||
// Permission model has HasAccess but NOT HasRoles
|
||||
$perm = Permission::create(['slug' => 'simple']);
|
||||
$article = Article::create(['title' => 'Solo']);
|
||||
|
||||
$perm->grantAccess($article);
|
||||
$this->assertTrue($perm->hasAccess($article));
|
||||
|
||||
// allAccess should work even without roles
|
||||
$accesses = $perm->allAccess();
|
||||
$this->assertCount(1, $accesses);
|
||||
}
|
||||
|
||||
// ─── Access entity/accessible relationships ──────────────────
|
||||
|
||||
public function test_access_entity_relationship(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'RelEntity']);
|
||||
|
||||
$access = $user->grantAccess($article);
|
||||
|
||||
$entity = $access->entity;
|
||||
$this->assertInstanceOf(User::class, $entity);
|
||||
$this->assertEquals($user->id, $entity->id);
|
||||
}
|
||||
|
||||
public function test_access_accessible_relationship(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$article = Article::create(['title' => 'RelAccessible']);
|
||||
|
||||
$access = $user->grantAccess($article);
|
||||
|
||||
$accessible = $access->accessible;
|
||||
$this->assertInstanceOf(Article::class, $accessible);
|
||||
$this->assertEquals($article->id, $accessible->id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,467 @@
|
|||
<?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' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
<?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 HasRolesTest 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' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function defineDatabaseMigrations(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
|
||||
}
|
||||
|
||||
// ─── roles relationship ──────────────────────────────────────
|
||||
|
||||
public function test_user_has_no_roles_by_default(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->assertCount(0, $user->roles);
|
||||
}
|
||||
|
||||
// ─── hasRole ─────────────────────────────────────────────────
|
||||
|
||||
public function test_has_role_returns_false_when_not_assigned(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
Role::create(['name' => 'Admin', 'slug' => 'admin']);
|
||||
|
||||
$this->assertFalse($user->hasRole('admin'));
|
||||
}
|
||||
|
||||
public function test_has_role_by_slug_string(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin', 'slug' => 'admin']);
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->assertTrue($user->hasRole('admin'));
|
||||
}
|
||||
|
||||
public function test_has_role_by_model_instance(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Editor', 'slug' => 'editor']);
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->assertTrue($user->hasRole($role));
|
||||
}
|
||||
|
||||
public function test_has_role_returns_false_for_nonexistent_slug(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->assertFalse($user->hasRole('nonexistent'));
|
||||
}
|
||||
|
||||
// ─── assignRole ──────────────────────────────────────────────
|
||||
|
||||
public function test_assign_role_by_string_creates_role(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('new-role');
|
||||
|
||||
$this->assertDatabaseHas('roles', ['slug' => 'new-role']);
|
||||
$this->assertTrue($user->hasRole('new-role'));
|
||||
}
|
||||
|
||||
public function test_assign_role_by_model_instance(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Writer', 'slug' => 'writer']);
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->assertTrue($user->hasRole($role));
|
||||
}
|
||||
|
||||
public function test_assign_role_by_numeric_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Viewer', 'slug' => 'viewer']);
|
||||
$user->assignRole($role->id);
|
||||
|
||||
$this->assertTrue($user->hasRole('viewer'));
|
||||
}
|
||||
|
||||
public function test_assign_role_respects_max_times_limit(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Limited', 'slug' => 'limited']);
|
||||
|
||||
// max_times = 1 (default), so second assign should be ignored
|
||||
$user->assignRole($role, 1);
|
||||
$user->assignRole($role, 1);
|
||||
|
||||
$count = DB::table(config('roles.table_names.role_member'))
|
||||
->where('member_id', $user->id)
|
||||
->where('member_type', $user->getMorphClass())
|
||||
->where('role_id', $role->id)
|
||||
->count();
|
||||
|
||||
$this->assertEquals(1, $count);
|
||||
}
|
||||
|
||||
public function test_assign_role_allows_duplicates_when_max_times_higher(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Stackable', 'slug' => 'stackable']);
|
||||
|
||||
$user->assignRole($role, 3);
|
||||
$user->assignRole($role, 3);
|
||||
$user->assignRole($role, 3);
|
||||
$user->assignRole($role, 3); // should be blocked
|
||||
|
||||
$count = DB::table(config('roles.table_names.role_member'))
|
||||
->where('member_id', $user->id)
|
||||
->where('member_type', $user->getMorphClass())
|
||||
->where('role_id', $role->id)
|
||||
->count();
|
||||
|
||||
$this->assertEquals(3, $count);
|
||||
}
|
||||
|
||||
public function test_assign_role_throws_on_invalid_argument(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Type-hinted as string|Role, so stdClass triggers TypeError
|
||||
$this->expectException(\TypeError::class);
|
||||
$user->assignRole(new \stdClass());
|
||||
}
|
||||
|
||||
// ─── removeRole ──────────────────────────────────────────────
|
||||
|
||||
public function test_remove_role_by_model(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Temp', 'slug' => 'temp']);
|
||||
$user->assignRole($role);
|
||||
$this->assertTrue($user->hasRole($role));
|
||||
|
||||
$user->removeRole($role);
|
||||
|
||||
// Refresh to clear cached relations
|
||||
$user->load('roles');
|
||||
$this->assertFalse($user->hasRole($role));
|
||||
}
|
||||
|
||||
public function test_remove_role_by_slug(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Removable', 'slug' => 'removable']);
|
||||
$user->assignRole($role);
|
||||
|
||||
$user->removeRole('removable');
|
||||
$user->load('roles');
|
||||
$this->assertFalse($user->hasRole('removable'));
|
||||
}
|
||||
|
||||
public function test_remove_role_does_not_affect_other_roles(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$keepRole = Role::create(['name' => 'Keep', 'slug' => 'keep']);
|
||||
$removeRole = Role::create(['name' => 'Remove', 'slug' => 'remove']);
|
||||
|
||||
$user->assignRole($keepRole);
|
||||
$user->assignRole($removeRole);
|
||||
|
||||
$user->removeRole($removeRole);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertTrue($user->hasRole($keepRole));
|
||||
$this->assertFalse($user->hasRole($removeRole));
|
||||
}
|
||||
|
||||
// ─── syncRoles ───────────────────────────────────────────────
|
||||
|
||||
public function test_sync_roles_by_slug_strings(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('old-role');
|
||||
|
||||
$user->syncRoles(['new-role-1', 'new-role-2']);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertFalse($user->hasRole('old-role'));
|
||||
$this->assertTrue($user->hasRole('new-role-1'));
|
||||
$this->assertTrue($user->hasRole('new-role-2'));
|
||||
}
|
||||
|
||||
public function test_sync_roles_by_model_instances(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role1 = Role::create(['name' => 'Role1', 'slug' => 'role1']);
|
||||
$role2 = Role::create(['name' => 'Role2', 'slug' => 'role2']);
|
||||
|
||||
$user->syncRoles([$role1, $role2]);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertTrue($user->hasRole($role1));
|
||||
$this->assertTrue($user->hasRole($role2));
|
||||
}
|
||||
|
||||
public function test_sync_roles_by_numeric_ids(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role1 = Role::create(['name' => 'R1', 'slug' => 'r1']);
|
||||
$role2 = Role::create(['name' => 'R2', 'slug' => 'r2']);
|
||||
|
||||
$user->syncRoles([$role1->id, $role2->id]);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertTrue($user->hasRole('r1'));
|
||||
$this->assertTrue($user->hasRole('r2'));
|
||||
}
|
||||
|
||||
public function test_sync_roles_by_objects_with_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'ObjRole', 'slug' => 'objrole']);
|
||||
|
||||
$user->syncRoles([(object) ['id' => $role->id]]);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertTrue($user->hasRole('objrole'));
|
||||
}
|
||||
|
||||
public function test_sync_roles_by_arrays_with_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'ArrRole', 'slug' => 'arrrole']);
|
||||
|
||||
$user->syncRoles([['id' => $role->id]]);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertTrue($user->hasRole('arrrole'));
|
||||
}
|
||||
|
||||
public function test_sync_roles_empty_removes_all(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('existing');
|
||||
|
||||
$user->syncRoles([]);
|
||||
$user->load('roles');
|
||||
|
||||
$this->assertCount(0, $user->roles);
|
||||
}
|
||||
|
||||
// ─── hasAnyRole ──────────────────────────────────────────────
|
||||
|
||||
public function test_has_any_role_returns_true_when_one_matches(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('editor');
|
||||
|
||||
$this->assertTrue($user->hasAnyRole(['admin', 'editor', 'viewer']));
|
||||
}
|
||||
|
||||
public function test_has_any_role_returns_false_when_none_match(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('writer');
|
||||
|
||||
$this->assertFalse($user->hasAnyRole(['admin', 'editor']));
|
||||
}
|
||||
|
||||
public function test_has_any_role_returns_false_for_empty_array(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->assertFalse($user->hasAnyRole([]));
|
||||
}
|
||||
|
||||
// ─── hasAllRoles ─────────────────────────────────────────────
|
||||
|
||||
public function test_has_all_roles_returns_true_when_all_match(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('admin');
|
||||
$user->assignRole('editor');
|
||||
|
||||
$this->assertTrue($user->hasAllRoles(['admin', 'editor']));
|
||||
}
|
||||
|
||||
public function test_has_all_roles_returns_false_when_one_missing(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('admin');
|
||||
|
||||
$this->assertFalse($user->hasAllRoles(['admin', 'editor']));
|
||||
}
|
||||
|
||||
public function test_has_all_roles_returns_true_for_empty_array(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->assertTrue($user->hasAllRoles([]));
|
||||
}
|
||||
|
||||
// ─── Expiration ──────────────────────────────────────────────
|
||||
|
||||
public function test_expired_role_is_not_returned_in_roles_relation(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Expired', 'slug' => 'expired']);
|
||||
|
||||
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->roles);
|
||||
$this->assertFalse($user->hasRole($role));
|
||||
}
|
||||
|
||||
public function test_non_expired_role_is_returned_in_roles_relation(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Future', 'slug' => 'future']);
|
||||
|
||||
DB::table(config('roles.table_names.role_member'))->insert([
|
||||
'role_id' => $role->id,
|
||||
'member_id' => $user->id,
|
||||
'member_type' => $user->getMorphClass(),
|
||||
'expires_at' => now()->addWeek(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $user->roles);
|
||||
$this->assertTrue($user->hasRole($role));
|
||||
}
|
||||
|
||||
// ─── extendOrAddRole ─────────────────────────────────────────
|
||||
|
||||
public function test_extend_or_add_role_creates_new_membership(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Extend', 'slug' => 'extend']);
|
||||
|
||||
$user->extendOrAddRole($role, 48);
|
||||
|
||||
$membership = DB::table(config('roles.table_names.role_member'))
|
||||
->where('role_id', $role->id)
|
||||
->where('member_id', $user->id)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($membership);
|
||||
$this->assertNotNull($membership->expires_at);
|
||||
}
|
||||
|
||||
public function test_extend_or_add_role_extends_existing_future_membership(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Ext', 'slug' => 'ext']);
|
||||
|
||||
// Create initial membership expiring in 24 hours
|
||||
DB::table(config('roles.table_names.role_member'))->insert([
|
||||
'role_id' => $role->id,
|
||||
'member_id' => $user->id,
|
||||
'member_type' => $user->getMorphClass(),
|
||||
'expires_at' => now()->addHours(24),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$user->extendOrAddRole($role, 24);
|
||||
|
||||
// Should now expire in ~48 hours from original base
|
||||
$membership = DB::table(config('roles.table_names.role_member'))
|
||||
->where('role_id', $role->id)
|
||||
->where('member_id', $user->id)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($membership);
|
||||
// Should be extended: approximately 48 hours from now
|
||||
$expiresAt = \Carbon\Carbon::parse($membership->expires_at);
|
||||
$this->assertTrue($expiresAt->gt(now()->addHours(40)));
|
||||
}
|
||||
|
||||
public function test_extend_or_add_role_does_not_modify_null_expiry(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'NoExp', 'slug' => 'noexp']);
|
||||
|
||||
// Create a permanent membership
|
||||
DB::table(config('roles.table_names.role_member'))->insert([
|
||||
'role_id' => $role->id,
|
||||
'member_id' => $user->id,
|
||||
'member_type' => $user->getMorphClass(),
|
||||
'expires_at' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$user->extendOrAddRole($role, 24);
|
||||
|
||||
$membership = DB::table(config('roles.table_names.role_member'))
|
||||
->where('role_id', $role->id)
|
||||
->where('member_id', $user->id)
|
||||
->first();
|
||||
|
||||
// Should still be null (permanent)
|
||||
$this->assertNull($membership->expires_at);
|
||||
}
|
||||
|
||||
public function test_extend_or_add_role_with_zero_hours_does_nothing(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'Zero', 'slug' => 'zero']);
|
||||
|
||||
$user->extendOrAddRole($role, 0);
|
||||
|
||||
$count = DB::table(config('roles.table_names.role_member'))
|
||||
->where('role_id', $role->id)
|
||||
->where('member_id', $user->id)
|
||||
->count();
|
||||
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
public function test_extend_or_add_role_by_slug_string(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
// extendOrAddRole resolves strings via firstOrCreate by name
|
||||
$role = Role::create(['name' => 'byslug', 'slug' => 'byslug']);
|
||||
|
||||
$user->extendOrAddRole('byslug', 12);
|
||||
|
||||
$this->assertTrue($user->hasRole('byslug'));
|
||||
}
|
||||
|
||||
public function test_extend_or_add_role_by_numeric_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$role = Role::create(['name' => 'ByNum', 'slug' => 'bynum']);
|
||||
|
||||
$user->extendOrAddRole($role->id, 12);
|
||||
|
||||
$this->assertTrue($user->hasRole('bynum'));
|
||||
}
|
||||
|
||||
// ─── Roles independence between users ────────────────────────
|
||||
|
||||
public function test_roles_are_independent_between_users(): void
|
||||
{
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
$user1->assignRole('admin');
|
||||
$user2->assignRole('editor');
|
||||
|
||||
$this->assertTrue($user1->hasRole('admin'));
|
||||
$this->assertFalse($user1->hasRole('editor'));
|
||||
$this->assertTrue($user2->hasRole('editor'));
|
||||
$this->assertFalse($user2->hasRole('admin'));
|
||||
}
|
||||
|
||||
// ─── Role slug uniqueness ────────────────────────────────────
|
||||
|
||||
public function test_role_slug_is_auto_suffixed_on_conflict(): void
|
||||
{
|
||||
$role1 = Role::create(['name' => 'Admin', 'slug' => 'admin']);
|
||||
$role2 = Role::create(['name' => 'Admin', 'slug' => 'admin']);
|
||||
|
||||
$this->assertEquals('admin', $role1->slug);
|
||||
$this->assertEquals('admin-1', $role2->slug);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Roles\Tests\Unit;
|
||||
|
||||
use Orchestra\Testbench\PHPUnit\TestCase;
|
||||
|
||||
// TODO complete this WIP
|
||||
class PermissionTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->loadLaravelMigrations(['--database' => 'testbench']);
|
||||
$this->artisan('migrate', ['--database' => 'testbench']);
|
||||
}
|
||||
|
||||
public function testHasPermission()
|
||||
{
|
||||
// Assuming you have a User model with the HasPermissions trait
|
||||
$user = \App\Models\User::factory()->create();
|
||||
|
||||
// Add a permission to the user
|
||||
$user->permissions()->attach(1, ['context' => 'test']);
|
||||
|
||||
// Check if the user has the permission
|
||||
$this->assertTrue($user->hasPermission('view_posts', ['context' => 'test']));
|
||||
$this->assertFalse($user->hasPermission('edit_posts', ['context' => 'test']));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue