permissionRepository = $this->createMock(PermissionRepositoryInterface::class); $this->rolePermissionRepository = $this->createMock(RolePermissionRepositoryInterface::class); $this->userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class); $this->systemAuditService = $this->createMock(AuditRecorderInterface::class); $this->sessionStore = $this->createMock(SessionStoreInterface::class); $this->service = new PermissionService( $this->permissionRepository, $this->rolePermissionRepository, $this->userRoleRepository, $this->systemAuditService, $this->sessionStore, ); } // ── userHas ────────────────────────────────────────────────────── public function testUserHasReturnsFalseForZeroUserId(): void { $this->assertFalse($this->service->userHas(0, 'users.view')); } public function testUserHasReturnsFalseForEmptyKey(): void { $this->assertFalse($this->service->userHas(1, '')); $this->assertFalse($this->service->userHas(1, ' ')); } public function testUserHasReturnsTrueWhenPermissionExists(): void { $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn(null); $this->userRoleRepository->expects($this->any())->method('listRoleIdsByUserId') ->with(5) ->willReturn([1, 2]); $this->rolePermissionRepository->expects($this->any())->method('listPermissionKeysByRoleIds') ->with([1, 2]) ->willReturn(['users.view', 'users.create']); $this->sessionStore->expects($this->atLeastOnce()) ->method('set') ->with('permissions', $this->callback(fn (array $v) => $v['user_id'] === 5 && $v['keys'] === ['users.view', 'users.create'])); $this->assertTrue($this->service->userHas(5, 'users.view')); } // ── getUserPermissions ─────────────────────────────────────────── public function testGetUserPermissionsReturnsEmptyForNonPositiveUserId(): void { $this->assertSame([], $this->service->getUserPermissions(0)); $this->assertSame([], $this->service->getUserPermissions(-1)); } public function testGetUserPermissionsUsesSessionCache(): void { $cached = ['user_id' => 7, 'keys' => ['settings.view']]; $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn($cached); // Repositories should NOT be called when cache is present. $this->userRoleRepository->expects($this->never())->method('listRoleIdsByUserId'); $this->rolePermissionRepository->expects($this->never())->method('listPermissionKeysByRoleIds'); $result = $this->service->getUserPermissions(7); $this->assertSame(['settings.view'], $result); } public function testGetUserPermissionsRefreshBypassesCache(): void { $cached = ['user_id' => 7, 'keys' => ['old.key']]; $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn($cached); $this->userRoleRepository->expects($this->any())->method('listRoleIdsByUserId') ->with(7) ->willReturn([3]); $this->rolePermissionRepository->expects($this->any())->method('listPermissionKeysByRoleIds') ->with([3]) ->willReturn(['new.key']); $this->sessionStore->expects($this->once()) ->method('set') ->with('permissions', $this->callback(fn (array $v) => $v['keys'] === ['new.key'])); $result = $this->service->getUserPermissions(7, true); $this->assertSame(['new.key'], $result); } // ── getCachedPermissions ───────────────────────────────────────── public function testGetCachedPermissionsReturnsEmptyWhenNoCache(): void { $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn(null); $this->assertSame([], $this->service->getCachedPermissions(1)); } public function testGetCachedPermissionsReturnsCachedKeysForMatchingUser(): void { $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn(['user_id' => 4, 'keys' => ['a', 'b']]); $this->assertSame(['a', 'b'], $this->service->getCachedPermissions(4)); } // ── clearUserCache ─────────────────────────────────────────────── public function testClearUserCacheRemovesSessionEntry(): void { $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn(['user_id' => 9, 'keys' => ['x']]); $this->sessionStore->expects($this->once()) ->method('remove') ->with('permissions'); $this->service->clearUserCache(9); } public function testClearUserCacheIgnoresMismatchedUserId(): void { $this->sessionStore->expects($this->any())->method('get') ->with('permissions') ->willReturn(['user_id' => 9, 'keys' => ['x']]); $this->sessionStore->expects($this->never())->method('remove'); $this->service->clearUserCache(42); } // ── Delegation methods ─────────────────────────────────────────── // ── createFromAdmin ────────────────────────────────────────────── public function testCreateFromAdminSucceeds(): void { $input = ['key' => 'custom.perm', 'description' => 'A custom permission', 'active' => '1', 'is_system' => '0']; $this->permissionRepository->expects($this->any())->method('findByKey')->with('custom.perm')->willReturn(null); $this->permissionRepository->expects($this->any())->method('create')->willReturn(42); $this->systemAuditService->expects($this->once())->method('record'); $result = $this->service->createFromAdmin($input); $this->assertTrue($result['ok']); $this->assertSame(42, $result['id']); $this->assertSame('custom.perm', $result['form']['key']); } public function testCreateFromAdminRejectsEmptyKey(): void { $result = $this->service->createFromAdmin(['key' => '', 'description' => '']); $this->assertFalse($result['ok']); $this->assertNotEmpty($result['errors']); } public function testCreateFromAdminRejectsInvalidKeyFormat(): void { $result = $this->service->createFromAdmin(['key' => 'invalid key!', 'description' => '']); $this->assertFalse($result['ok']); $this->assertNotEmpty($result['errors']); } public function testCreateFromAdminRejectsDuplicateKey(): void { $this->permissionRepository->expects($this->any())->method('findByKey') ->with('users.view') ->willReturn(['id' => 1, 'key' => 'users.view']); $result = $this->service->createFromAdmin(['key' => 'users.view', 'description' => '']); $this->assertFalse($result['ok']); $this->assertNotEmpty($result['errors']); } // ── updateFromAdmin ────────────────────────────────────────────── public function testUpdateFromAdminSucceeds(): void { $input = ['key' => 'custom.perm', 'description' => 'Updated', 'active' => '1', 'is_system' => '0']; // findByKey returns the same id → no uniqueness conflict $this->permissionRepository->expects($this->any())->method('findByKey') ->with('custom.perm') ->willReturn(['id' => 10, 'key' => 'custom.perm']); $this->permissionRepository->expects($this->any())->method('find') ->with(10) ->willReturn(['id' => 10, 'key' => 'custom.perm', 'active' => 0]); $this->permissionRepository->expects($this->any())->method('update')->with(10, $this->isArray())->willReturn(true); $this->systemAuditService->expects($this->once())->method('record'); $result = $this->service->updateFromAdmin(10, $input); $this->assertTrue($result['ok']); } public function testUpdateFromAdminRejectsDuplicateKeyFromAnotherPermission(): void { $this->permissionRepository->expects($this->any())->method('findByKey') ->with('taken.key') ->willReturn(['id' => 99, 'key' => 'taken.key']); $result = $this->service->updateFromAdmin(10, ['key' => 'taken.key', 'description' => '']); $this->assertFalse($result['ok']); $this->assertNotEmpty($result['errors']); } // ── deleteById ─────────────────────────────────────────────────── public function testDeleteByIdSucceeds(): void { $permission = ['id' => 5, 'key' => 'custom.perm', 'is_system' => 0]; $this->permissionRepository->expects($this->any())->method('find')->with(5)->willReturn($permission); $this->permissionRepository->expects($this->any())->method('delete')->with(5)->willReturn(true); $this->systemAuditService->expects($this->once())->method('record'); $result = $this->service->deleteById(5); $this->assertTrue($result['ok']); $this->assertSame($permission, $result['permission']); } public function testDeleteByIdProtectsSystemPermission(): void { $permission = ['id' => 1, 'key' => 'users.view', 'is_system' => 1]; $this->permissionRepository->expects($this->any())->method('find')->with(1)->willReturn($permission); $result = $this->service->deleteById(1); $this->assertFalse($result['ok']); $this->assertSame(403, $result['status']); $this->assertSame('system_permission_protected', $result['error']); } public function testDeleteByIdReturnsNotFoundForMissing(): void { $this->permissionRepository->expects($this->any())->method('find')->with(999)->willReturn(null); $result = $this->service->deleteById(999); $this->assertFalse($result['ok']); $this->assertSame(404, $result['status']); $this->assertSame('not_found', $result['error']); } }