'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.'); $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");