AccessServiceTest.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. namespace Tests\Unit\Services;
  3. use App\Models\Permission;
  4. use App\Models\Role;
  5. use App\Models\User;
  6. use App\Services\Access\AccessService;
  7. use Database\Seeders\RbacSeeder;
  8. use Illuminate\Foundation\Testing\RefreshDatabase;
  9. use Tests\TestCase;
  10. class AccessServiceTest extends TestCase
  11. {
  12. use RefreshDatabase;
  13. protected $seed = true;
  14. public function test_rbac_seeder_backfills_user_role_id(): void
  15. {
  16. $user = User::factory()->create(['role' => Role::MANAGER]);
  17. $this->seed(RbacSeeder::class);
  18. $user->refresh();
  19. $this->assertNotNull($user->role_id);
  20. $this->assertSame(Role::MANAGER, $user->roleModel->slug);
  21. }
  22. public function test_admin_role_has_seeded_permissions(): void
  23. {
  24. $admin = User::factory()->create(['role' => Role::ADMIN]);
  25. $this->seed(RbacSeeder::class);
  26. $admin->refresh();
  27. $this->assertTrue(app(AccessService::class)->can($admin, 'catalog.delete'));
  28. $this->assertTrue(app(AccessService::class)->can($admin, 'catalog.fields.product_price.update'));
  29. }
  30. public function test_manager_has_seeded_permissions_but_not_admin_only_permissions(): void
  31. {
  32. $manager = User::factory()->create(['role' => Role::MANAGER]);
  33. $this->seed(RbacSeeder::class);
  34. $manager->refresh();
  35. $this->assertTrue(app(AccessService::class)->can($manager, 'catalog.view'));
  36. $this->assertTrue(app(AccessService::class)->can($manager, 'catalog.fields.product_price.view'));
  37. $this->assertFalse(app(AccessService::class)->can($manager, 'catalog.import'));
  38. $this->assertFalse(app(AccessService::class)->can($manager, 'catalog.update'));
  39. }
  40. public function test_seeded_roles_have_visibility_scopes(): void
  41. {
  42. $manager = User::factory()->create(['role' => Role::MANAGER]);
  43. $brigadier = User::factory()->create(['role' => Role::BRIGADIER]);
  44. $warehouseHead = User::factory()->create(['role' => Role::WAREHOUSE_HEAD]);
  45. $this->seed(RbacSeeder::class);
  46. $this->assertSame('manager', $manager->refresh()->visibilityScope('orders'));
  47. $this->assertSame('brigadier', $brigadier->refresh()->visibilityScope('orders'));
  48. $this->assertSame('warehouse_head', $warehouseHead->refresh()->visibilityScope('orders'));
  49. $this->assertSame('manager', $warehouseHead->visibilityScope('schedule'));
  50. }
  51. public function test_visibility_scope_uses_highest_priority_permission(): void
  52. {
  53. $this->seed(RbacSeeder::class);
  54. $role = Role::query()->create([
  55. 'slug' => 'hybrid_scope',
  56. 'name' => 'Hybrid scope',
  57. 'is_system' => false,
  58. 'is_active' => true,
  59. ]);
  60. $permissions = Permission::query()
  61. ->whereIn('slug', ['orders.scope.brigadier', 'orders.scope.manager'])
  62. ->pluck('id')
  63. ->mapWithKeys(fn (int $id): array => [$id => ['effect' => 'allow']]);
  64. $role->permissions()->sync($permissions);
  65. $user = User::factory()->create(['role' => $role->slug, 'role_id' => $role->id]);
  66. $this->assertSame('manager', app(AccessService::class)->visibilityScope($user, 'orders'));
  67. }
  68. public function test_role_deny_overrides_user_allow(): void
  69. {
  70. $this->seed(RbacSeeder::class);
  71. $permission = Permission::query()->where('slug', 'catalog.view')->firstOrFail();
  72. $role = Role::query()->create([
  73. 'slug' => 'deny_catalog',
  74. 'name' => 'Deny catalog',
  75. 'is_system' => false,
  76. 'is_active' => true,
  77. ]);
  78. $role->permissions()->sync([
  79. $permission->id => ['effect' => 'deny'],
  80. ]);
  81. $user = User::factory()->create(['role' => $role->slug, 'role_id' => $role->id]);
  82. $user->permissions()->sync([
  83. $permission->id => ['effect' => 'allow'],
  84. ]);
  85. app(AccessService::class)->bumpCacheVersion();
  86. $this->assertFalse(app(AccessService::class)->can($user, 'catalog.view'));
  87. }
  88. public function test_exact_route_permission_lookup_supports_dotted_route_names(): void
  89. {
  90. $this->assertSame(
  91. 'catalog.search',
  92. app(AccessService::class)->routePermission('product.search')
  93. );
  94. }
  95. public function test_user_deny_overrides_role_allow(): void
  96. {
  97. $manager = User::factory()->create(['role' => Role::MANAGER]);
  98. $this->seed(RbacSeeder::class);
  99. $manager->refresh();
  100. $permission = Permission::query()->where('slug', 'catalog.view')->firstOrFail();
  101. $manager->permissions()->syncWithoutDetaching([
  102. $permission->id => ['effect' => 'deny'],
  103. ]);
  104. app(AccessService::class)->bumpCacheVersion();
  105. $this->assertFalse(app(AccessService::class)->can($manager, 'catalog.view'));
  106. }
  107. public function test_assistant_head_has_materialized_admin_permissions_without_runtime_inheritance(): void
  108. {
  109. $assistantHead = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
  110. $this->seed(RbacSeeder::class);
  111. $assistantHead->refresh();
  112. $this->assertTrue(app(AccessService::class)->can($assistantHead, 'maf_orders.delete'));
  113. }
  114. public function test_custom_role_with_admin_permissions_has_same_action_access(): void
  115. {
  116. $this->seed(RbacSeeder::class);
  117. $role = Role::query()->create([
  118. 'slug' => 'root',
  119. 'name' => 'Root',
  120. 'is_system' => false,
  121. 'is_active' => true,
  122. ]);
  123. $permissions = Permission::query()
  124. ->pluck('id')
  125. ->mapWithKeys(fn (int $id): array => [$id => ['effect' => 'allow']]);
  126. $role->permissions()->sync($permissions);
  127. $user = User::factory()->create(['role' => $role->slug, 'role_id' => $role->id]);
  128. $this->assertTrue(app(AccessService::class)->can($user, 'admin.roles'));
  129. $this->assertTrue(app(AccessService::class)->can($user, 'catalog.delete'));
  130. $this->assertTrue(app(AccessService::class)->can($user, 'orders.documents.generate'));
  131. $this->assertSame('admin', app(AccessService::class)->visibilityScope($user, 'orders'));
  132. }
  133. public function test_user_permission_query_scopes_include_custom_roles(): void
  134. {
  135. $this->seed(RbacSeeder::class);
  136. $role = Role::query()->create([
  137. 'slug' => 'root_scope',
  138. 'name' => 'Root scope',
  139. 'is_system' => false,
  140. 'is_active' => true,
  141. ]);
  142. $permission = Permission::query()->where('slug', 'orders.scope.admin')->firstOrFail();
  143. $role->permissions()->sync([$permission->id => ['effect' => 'allow']]);
  144. $user = User::factory()->create(['role' => $role->slug, 'role_id' => $role->id]);
  145. $this->assertTrue(User::query()->withPermission('orders.scope.admin')->whereKey($user->id)->exists());
  146. }
  147. }