'Support Team', 'description' => 'Handles customer support', ], $roles->normalize([ 'name' => ' Support Team ', 'description' => ' Handles customer support ', 'permissions' => ['users.manage'], ]), 'Role normalization should trim supported fields and ignore unrelated fields.'); $valid = $roles->validate(['name' => 'Support Team', 'description' => str_repeat('x', 255)]); role_permission_assert_same(true, $valid['valid'], 'A schema-sized custom role should validate.'); role_permission_assert_same([], $valid['errors'], 'Valid role should have no errors.'); $invalid = $roles->validate(['name' => "\x01", 'description' => str_repeat('x', 256)]); role_permission_assert_same(false, $invalid['valid'], 'Invalid role data should report invalid.'); if (!isset($invalid['errors']['name'], $invalid['errors']['description'])) { throw new RuntimeException('Role validation should report name and description errors.'); } role_permission_assert_throws( fn() => $roles->assertCanRename(['name' => 'Administrator'], 'Security Administrator'), 'Administrator must not be renamed.' ); role_permission_assert_throws( fn() => $roles->assertCanDelete(['name' => ' administrator ']), 'Administrator must not be deleted.' ); role_permission_assert_throws( fn() => $roles->assertCanChangePermissions(['name' => 'Administrator']), 'Administrator permissions must not be changed.' ); role_permission_assert_throws( fn() => $roles->assertCanRename(['name' => 'Accounts'], ' administrator '), 'A custom role must not be renamed to Administrator.' ); role_permission_assert_same(true, $roles->canRename(['name' => 'Accounts'], 'Support'), 'Custom roles should be renameable.'); role_permission_assert_same(false, $roles->canDelete(['name' => 'Administrator']), 'Administrator deletion safeguard should be queryable.'); // ID 1 is authoritative for the protected role; current name is not required. $adminEdit = (new App\Domain\User\RolePermissionService())->validateEdit(1, [ 'name' => 'Renamed Administrator', 'description' => 'changed', 'permissions' => ['clients.view'], ]); role_permission_assert_same(false, $adminEdit['valid'], 'Role ID 1 must remain protected without current_name.'); if (!isset($adminEdit['errors']['role'])) throw new RuntimeException('Administrator rename/permission changes must be rejected from ID alone.'); role_permission_assert_same(false, $roles->canDelete(['id' => 1]), 'Role ID 1 must not be deletable without a name.'); $customCreate = (new App\Domain\User\RolePermissionService())->validateForCreate([ 'name' => ' Dispatch ', 'description' => ' Handles dispatch ', 'permissions' => ['CLIENTS.VIEW'], ], ['clients.view']); role_permission_assert_same(true, $customCreate['valid'], 'Custom role create DTO should validate and normalize permissions.'); role_permission_assert_same('Dispatch', $customCreate['name'], 'Custom role names should be normalized in create DTOs.'); role_permission_assert_same(['clients.view'], $customCreate['permissions'], 'Create DTO should include canonical permissions.'); $customDelete = (new App\Domain\User\RolePermissionService())->validateDelete(4, ['id' => 4, 'name' => 'Dispatch']); role_permission_assert_same(['valid' => true, 'id' => 4, 'errors' => []], $customDelete, 'Custom role delete DTO should be safe and controller-ready.'); $display = $roles->display([ 'id' => 4, 'name' => 'Support Team', 'description' => 'Handles support', 'created_at' => '2026-09-01 08:00:00', 'permissions' => ['clients.view'], 'password_hash' => 'secret', 'internal_notes' => 'omit', ]); role_permission_assert_same([ 'id' => 4, 'name' => 'Support Team', 'description' => 'Handles support', 'created_at' => '2026-09-01 08:00:00', 'permissions' => ['clients.view'], ], $display, 'Role display projection must allow-list safe fields.'); $permissions = new PermissionMatrix(); role_permission_assert_same([ 'clients.view', 'jobcards.manage', 'reports.view', ], $permissions->normalize([ ' clients.view ', 'jobcards.manage', 'clients.view', '', 'reports.view', ' ', ]), 'Permissions should be trimmed, blank entries removed, and duplicates de-duplicated.'); role_permission_assert_same([ 'clients.view', 'jobcards.manage', ], $permissions->normalize(['CLIENTS.VIEW', 'clients.view', ' jobcards.manage ']), 'Permission normalization should use canonical lower-case names.'); role_permission_assert_same([ 'id' => 4, 'name' => 'Support Team', 'permissions' => ['clients.view', 'jobcards.manage'], ], $permissions->display([ 'id' => 4, 'name' => 'Support Team', 'permissions' => ['clients.view', 'jobcards.manage', 'clients.view'], 'password' => 'secret', 'token' => 'secret', ]), 'Permission display projection must be safe and normalized.'); printf("Role and permission tests: 10 passed\n");