keycloak-api-manager 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/Handlers/attackDetectionHandler.js +64 -0
  2. package/Handlers/clientPoliciesHandler.js +120 -0
  3. package/Handlers/groupsHandler.js +32 -0
  4. package/Handlers/organizationsHandler.js +243 -0
  5. package/Handlers/serverInfoHandler.js +36 -0
  6. package/Handlers/userProfileHandler.js +121 -0
  7. package/README.md +83 -7157
  8. package/docs/architecture.md +47 -0
  9. package/docs/deployment.md +32 -0
  10. package/docs/keycloak-setup.md +47 -0
  11. package/docs/test-configuration.md +43 -0
  12. package/docs/testing.md +60 -0
  13. package/index.js +156 -240
  14. package/package.json +28 -15
  15. package/test/.mocharc.json +2 -2
  16. package/test/config/secrets.json +12 -0
  17. package/test/docker-keycloak/certs/keycloak.crt +58 -0
  18. package/test/docker-keycloak/certs/keycloak.key +28 -0
  19. package/test/docker-keycloak/docker-compose-https.yml +2 -0
  20. package/test/docker-keycloak/docker-compose.yml +4 -4
  21. package/test/helpers/matrix.js +16 -0
  22. package/test/matrix/auth.json +27 -0
  23. package/test/matrix/clients.json +45 -0
  24. package/test/matrix/realms-components-idp.json +37 -0
  25. package/test/matrix/users-roles-groups.json +26 -0
  26. package/test/package-lock.json +3032 -0
  27. package/test/specs/attackDetection.test.js +102 -0
  28. package/test/specs/clientCredentials.test.js +79 -0
  29. package/test/specs/clientPolicies.test.js +162 -0
  30. package/test/specs/{debugClientLibrary.test.js → diagnostics/debugClientLibrary.test.js} +2 -2
  31. package/test/specs/groupPermissions.test.js +87 -0
  32. package/test/specs/matrix/matrix-auth.test.js +112 -0
  33. package/test/specs/matrix/matrix-clients.test.js +59 -0
  34. package/test/specs/matrix/matrix-realms-components-idp.test.js +111 -0
  35. package/test/specs/matrix/matrix-users-roles-groups.test.js +68 -0
  36. package/test/specs/organizations.test.js +183 -0
  37. package/test/specs/serverInfo.test.js +140 -0
  38. package/test/specs/userProfile.test.js +135 -0
  39. package/test/{enableServerFeatures.js → support/enableServerFeatures.js} +43 -26
  40. package/test/{setup.js → support/setup.js} +3 -3
  41. package/test/support/testConfig.js +69 -0
  42. package/test/testConfig.js +1 -69
  43. package/test-output.log +72 -0
  44. package/test/TESTING.md +0 -327
  45. package/test/config/CONFIGURATION.md +0 -170
  46. package/test/diagnostic-protocol-mappers.js +0 -189
  47. package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +0 -262
  48. package/test/helpers/setup.js +0 -186
@@ -0,0 +1,111 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', '..', 'config');
6
+
7
+ const keycloakManager = require('keycloak-api-manager');
8
+ const { KEYCLOAK_CONFIG } = require('../../testConfig');
9
+ const { loadMatrix, uniqueName } = require('../../helpers/matrix');
10
+
11
+ function shouldSkipFeature(err) {
12
+ if (!err || !err.message) {
13
+ return false;
14
+ }
15
+ const text = err.message.toLowerCase();
16
+ return (
17
+ (text.includes('provider') && text.includes('not found')) ||
18
+ text.includes('feature not enabled') ||
19
+ text.includes('not supported') ||
20
+ text.includes('http 404') ||
21
+ text.includes('unknown_error')
22
+ );
23
+ }
24
+
25
+ describe('Matrix - Realms, Components, Identity Providers', function () {
26
+ this.timeout(60000);
27
+
28
+ const matrix = loadMatrix('realms-components-idp');
29
+
30
+ before(async function () {
31
+ await keycloakManager.configure({
32
+ baseUrl: KEYCLOAK_CONFIG.baseUrl,
33
+ realmName: KEYCLOAK_CONFIG.realmName,
34
+ clientId: KEYCLOAK_CONFIG.clientId,
35
+ clientSecret: KEYCLOAK_CONFIG.clientSecret,
36
+ username: KEYCLOAK_CONFIG.username,
37
+ password: KEYCLOAK_CONFIG.password,
38
+ grantType: KEYCLOAK_CONFIG.grantType,
39
+ tokenLifeSpan: KEYCLOAK_CONFIG.tokenLifeSpan,
40
+ scope: KEYCLOAK_CONFIG.scope,
41
+ });
42
+ });
43
+
44
+ matrix.realms.forEach((testCase) => {
45
+ it(`realm case: ${testCase.name}`, async function () {
46
+ const realmName = uniqueName(`matrix-realm-${testCase.name}`);
47
+
48
+ await keycloakManager.realms.create({
49
+ realm: realmName,
50
+ enabled: true,
51
+ ...testCase.realmConfig,
52
+ });
53
+
54
+ const realms = await keycloakManager.realms.find();
55
+ expect(realms.map((r) => r.realm)).to.include(realmName);
56
+
57
+ await keycloakManager.realms.update(
58
+ { realm: realmName },
59
+ {
60
+ ...testCase.realmConfig,
61
+ displayName: `Updated ${realmName}`,
62
+ }
63
+ );
64
+
65
+ const updated = await keycloakManager.realms.findOne({ realm: realmName });
66
+ expect(updated.displayName).to.equal(`Updated ${realmName}`);
67
+
68
+ await keycloakManager.realms.del({ realm: realmName });
69
+ });
70
+ });
71
+
72
+ it('components: find and check components exist', async function () {
73
+ const components = await keycloakManager.components.find();
74
+ expect(components).to.be.an('array');
75
+ });
76
+
77
+ matrix.identityProviders.forEach((testCase) => {
78
+ it(`idp case: ${testCase.name}`, async function () {
79
+ const realmName = uniqueName(`matrix-idp-realm-${testCase.name}`);
80
+ const alias = uniqueName(`matrix-idp-${testCase.name}`);
81
+
82
+ await keycloakManager.realms.create({ realm: realmName, enabled: true });
83
+ keycloakManager.setConfig({ realmName });
84
+
85
+ try {
86
+ await keycloakManager.identityProviders.create({
87
+ alias,
88
+ providerId: testCase.providerId,
89
+ enabled: true,
90
+ trustEmail: false,
91
+ storeToken: false,
92
+ addReadTokenRoleOnCreate: false,
93
+ authenticateByDefault: false,
94
+ config: testCase.config,
95
+ });
96
+ } catch (err) {
97
+ if (shouldSkipFeature(err)) {
98
+ this.skip();
99
+ return;
100
+ }
101
+ throw err;
102
+ }
103
+
104
+ const idps = await keycloakManager.identityProviders.find();
105
+ expect(idps.map((i) => i.alias)).to.include(alias);
106
+
107
+ await keycloakManager.identityProviders.del({ alias });
108
+ await keycloakManager.realms.del({ realm: realmName });
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,68 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', '..', 'config');
6
+
7
+ const keycloakManager = require('keycloak-api-manager');
8
+ const { TEST_REALM } = require('../../testConfig');
9
+ const { loadMatrix, uniqueName } = require('../../helpers/matrix');
10
+
11
+ describe('Matrix - Users, Roles, Groups', function () {
12
+ this.timeout(30000);
13
+
14
+ const matrix = loadMatrix('users-roles-groups');
15
+
16
+ before(function () {
17
+ keycloakManager.setConfig({ realmName: TEST_REALM });
18
+ });
19
+
20
+ matrix.cases.forEach((testCase) => {
21
+ it(`user-role-group case: ${testCase.name}`, async function () {
22
+ const roleName = uniqueName(`matrix-role-${testCase.name}`);
23
+ const groupName = uniqueName(`matrix-group-${testCase.name}`);
24
+ const username = uniqueName(`matrix-user-${testCase.name}`);
25
+ const email = `${username}@example.test`;
26
+
27
+ await keycloakManager.roles.create({
28
+ name: roleName,
29
+ description: `Role for ${testCase.name}`,
30
+ });
31
+
32
+ const group = await keycloakManager.groups.create({
33
+ name: groupName,
34
+ attributes: { description: ['Matrix group'] },
35
+ });
36
+
37
+ const user = await keycloakManager.users.create({
38
+ username,
39
+ email,
40
+ enabled: testCase.user.enabled,
41
+ emailVerified: testCase.user.emailVerified,
42
+ firstName: 'Matrix',
43
+ lastName: 'User',
44
+ });
45
+
46
+ await keycloakManager.users.addToGroup({
47
+ id: user.id,
48
+ groupId: group.id,
49
+ });
50
+
51
+ const role = await keycloakManager.roles.findOneByName({ name: roleName });
52
+ await keycloakManager.users.addRealmRoleMappings({
53
+ id: user.id,
54
+ roles: [role],
55
+ });
56
+
57
+ const userGroups = await keycloakManager.users.listGroups({ id: user.id });
58
+ expect(userGroups.map((g) => g.id)).to.include(group.id);
59
+
60
+ const roleMappings = await keycloakManager.users.listRealmRoleMappings({ id: user.id });
61
+ expect(roleMappings.map((r) => r.name)).to.include(roleName);
62
+
63
+ await keycloakManager.users.del({ id: user.id });
64
+ await keycloakManager.groups.del({ id: group.id });
65
+ await keycloakManager.roles.delByName({ name: roleName });
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,183 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', 'config');
6
+
7
+ const { conf } = require('propertiesmanager');
8
+ const KeycloakManager = require('../../index');
9
+ const { TEST_REALM, generateUniqueName } = require('../testConfig');
10
+
11
+ const config = {
12
+ baseUrl: conf.keycloak.baseUrl,
13
+ realmName: conf.keycloak.realmName,
14
+ clientId: conf.keycloak.clientId,
15
+ clientSecret: conf.keycloak.clientSecret,
16
+ grantType: conf.keycloak.grantType,
17
+ username: conf.keycloak.username,
18
+ password: conf.keycloak.password,
19
+ tokenLifeSpan: conf.keycloak.tokenLifeSpan
20
+ };
21
+
22
+ /**
23
+ * Integration tests for Organizations Handler
24
+ * Tests organization CRUD, members, identity providers (Keycloak 25+)
25
+ */
26
+ describe('Organizations Handler Tests', function () {
27
+ this.timeout(10000);
28
+
29
+ const testOrgData = {
30
+ name: `test-org-${Date.now()}`,
31
+ domains: [],
32
+ attributes: {
33
+ customAttr: ['value1']
34
+ }
35
+ };
36
+
37
+ let createdOrgId;
38
+ let testUserId;
39
+
40
+ before(async function () {
41
+ await KeycloakManager.configure(config);
42
+
43
+ // Switch to test realm for operations
44
+ KeycloakManager.setConfig({ realmName: TEST_REALM });
45
+
46
+ // Create a test user for member operations in test realm
47
+ const userResult = await KeycloakManager.users.create({
48
+ username: `org-test-user-${Date.now()}`,
49
+ email: 'orguser@test.com',
50
+ enabled: true
51
+ });
52
+ testUserId = userResult.id;
53
+ });
54
+
55
+ after(async function () {
56
+ // Clean up
57
+ if (createdOrgId) {
58
+ try {
59
+ await KeycloakManager.organizations.del({ id: createdOrgId });
60
+ } catch (e) {
61
+ // Org might already be deleted
62
+ }
63
+ }
64
+ if (testUserId) {
65
+ try {
66
+ await KeycloakManager.users.del({ id: testUserId });
67
+ } catch (e) {
68
+ // User might already be deleted
69
+ }
70
+ }
71
+ KeycloakManager.stop();
72
+ });
73
+
74
+ describe('Organization CRUD Operations', function () {
75
+ it('should create an organization', async function () {
76
+ const result = await KeycloakManager.organizations.create(testOrgData);
77
+ expect(result).to.have.property('id');
78
+ createdOrgId = result.id;
79
+ });
80
+
81
+ it('should find all organizations', async function () {
82
+ if (!createdOrgId) this.skip();
83
+
84
+ const orgs = await KeycloakManager.organizations.find({});
85
+ expect(orgs).to.be.an('array');
86
+ expect(orgs.length).to.be.greaterThan(0);
87
+ });
88
+
89
+ it('should find one organization by ID', async function () {
90
+ if (!createdOrgId) this.skip();
91
+
92
+ const org = await KeycloakManager.organizations.findOne({ id: createdOrgId });
93
+ expect(org).to.have.property('id', createdOrgId);
94
+ expect(org).to.have.property('name', testOrgData.name);
95
+ });
96
+
97
+ it('should update an organization', async function () {
98
+ if (!createdOrgId) this.skip();
99
+
100
+ const updatedData = {
101
+ attributes: {
102
+ customAttr: ['updatedValue']
103
+ }
104
+ };
105
+
106
+ await KeycloakManager.organizations.update({ id: createdOrgId }, updatedData);
107
+
108
+ const org = await KeycloakManager.organizations.findOne({ id: createdOrgId });
109
+ expect(org.attributes.customAttr).to.deep.equal(['updatedValue']);
110
+ });
111
+ });
112
+
113
+ describe('Organization Members', function () {
114
+ it('should add a member to organization', async function () {
115
+ if (!createdOrgId || !testUserId) this.skip();
116
+
117
+ await KeycloakManager.organizations.addMember({
118
+ id: createdOrgId,
119
+ userId: testUserId
120
+ });
121
+
122
+ expect(true).to.be.true;
123
+ });
124
+
125
+ it('should list organization members', async function () {
126
+ if (!createdOrgId) this.skip();
127
+
128
+ const members = await KeycloakManager.organizations.listMembers({
129
+ id: createdOrgId
130
+ });
131
+
132
+ expect(members).to.be.an('array');
133
+ });
134
+
135
+ it('should remove a member from organization', async function () {
136
+ if (!createdOrgId || !testUserId) this.skip();
137
+
138
+ await KeycloakManager.organizations.delMember({
139
+ id: createdOrgId,
140
+ userId: testUserId
141
+ });
142
+
143
+ expect(true).to.be.true;
144
+ });
145
+ });
146
+
147
+ describe('Organization Identity Providers', function () {
148
+ it('should list identity providers for organization', async function () {
149
+ if (!createdOrgId) this.skip();
150
+
151
+ try {
152
+ const idps = await KeycloakManager.organizations.listIdentityProviders({
153
+ id: createdOrgId
154
+ });
155
+
156
+ expect(idps).to.be.an('array');
157
+ } catch (error) {
158
+ // Method might not be available
159
+ if (error.response?.status === 404) {
160
+ this.skip();
161
+ }
162
+ throw error;
163
+ }
164
+ });
165
+ });
166
+
167
+ describe('Organization Deletion', function () {
168
+ it('should delete an organization', async function () {
169
+ if (!createdOrgId) this.skip();
170
+
171
+ await KeycloakManager.organizations.del({ id: createdOrgId });
172
+
173
+ try {
174
+ await KeycloakManager.organizations.findOne({ id: createdOrgId });
175
+ throw new Error('Organization should have been deleted');
176
+ } catch (error) {
177
+ expect(error.response?.status).to.equal(404);
178
+ }
179
+
180
+ createdOrgId = null; // Prevent cleanup attempt
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,140 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', 'config');
6
+
7
+ const { conf } = require('propertiesmanager');
8
+ const KeycloakManager = require('../../index');
9
+ const { TEST_REALM, generateUniqueName } = require('../testConfig');
10
+
11
+ const config = {
12
+ baseUrl: conf.keycloak.baseUrl,
13
+ realmName: conf.keycloak.realmName,
14
+ clientId: conf.keycloak.clientId,
15
+ clientSecret: conf.keycloak.clientSecret,
16
+ grantType: conf.keycloak.grantType,
17
+ username: conf.keycloak.username,
18
+ password: conf.keycloak.password,
19
+ tokenLifeSpan: conf.keycloak.tokenLifeSpan
20
+ };
21
+
22
+ /**
23
+ * Integration tests for Server Info Handler
24
+ * Tests server information and metadata retrieval
25
+ */
26
+ describe('Server Info Handler Tests', function () {
27
+ this.timeout(10000);
28
+
29
+ before(async function () {
30
+ await KeycloakManager.configure(config);
31
+ });
32
+
33
+ after(async function () {
34
+ KeycloakManager.stop();
35
+ });
36
+
37
+ describe('Server Information', function () {
38
+ let serverInfo;
39
+
40
+ it('should get comprehensive server information', async function () {
41
+ serverInfo = await KeycloakManager.serverInfo.getInfo();
42
+
43
+ expect(serverInfo).to.exist;
44
+ expect(serverInfo).to.be.an('object');
45
+ });
46
+
47
+ it('should contain system information', async function () {
48
+ if (!serverInfo) this.skip();
49
+
50
+ expect(serverInfo).to.have.property('systemInfo');
51
+ expect(serverInfo.systemInfo).to.be.an('object');
52
+
53
+ // System info typically includes version, uptime, etc.
54
+ if (serverInfo.systemInfo.version) {
55
+ expect(serverInfo.systemInfo.version).to.be.a('string');
56
+ }
57
+ });
58
+
59
+ it('should contain memory information', async function () {
60
+ if (!serverInfo) this.skip();
61
+
62
+ expect(serverInfo).to.have.property('memoryInfo');
63
+ expect(serverInfo.memoryInfo).to.be.an('object');
64
+ });
65
+
66
+ it('should contain profile information', async function () {
67
+ if (!serverInfo) this.skip();
68
+
69
+ expect(serverInfo).to.have.property('profileInfo');
70
+ expect(serverInfo.profileInfo).to.be.an('object');
71
+ });
72
+
73
+ it('should contain available themes', async function () {
74
+ if (!serverInfo) this.skip();
75
+
76
+ expect(serverInfo).to.have.property('themes');
77
+ expect(serverInfo.themes).to.be.an('object');
78
+
79
+ // Themes should include login, account, admin, etc.
80
+ const themeTypes = Object.keys(serverInfo.themes);
81
+ expect(themeTypes.length).to.be.greaterThan(0);
82
+ });
83
+
84
+ it('should contain available providers', async function () {
85
+ if (!serverInfo) this.skip();
86
+
87
+ expect(serverInfo).to.have.property('providers');
88
+ expect(serverInfo.providers).to.be.an('object');
89
+
90
+ // Providers should include various SPIs
91
+ const providerTypes = Object.keys(serverInfo.providers);
92
+ expect(providerTypes.length).to.be.greaterThan(0);
93
+ });
94
+
95
+ it('should contain protocol mapper types', async function () {
96
+ if (!serverInfo) this.skip();
97
+
98
+ expect(serverInfo).to.have.property('protocolMapperTypes');
99
+ expect(serverInfo.protocolMapperTypes).to.be.an('object');
100
+ });
101
+
102
+ it('should contain component types', async function () {
103
+ if (!serverInfo) this.skip();
104
+
105
+ if (serverInfo.componentTypes) {
106
+ expect(serverInfo.componentTypes).to.be.an('object');
107
+ }
108
+ });
109
+
110
+ it('should contain password policies', async function () {
111
+ if (!serverInfo) this.skip();
112
+
113
+ if (serverInfo.passwordPolicies) {
114
+ expect(serverInfo.passwordPolicies).to.be.an('array');
115
+ }
116
+ });
117
+
118
+ it('should contain enums', async function () {
119
+ if (!serverInfo) this.skip();
120
+
121
+ if (serverInfo.enums) {
122
+ expect(serverInfo.enums).to.be.an('object');
123
+ }
124
+ });
125
+
126
+ it('should verify specific provider categories exist', async function () {
127
+ if (!serverInfo || !serverInfo.providers) this.skip();
128
+
129
+ const providers = serverInfo.providers;
130
+
131
+ // Check for common provider categories
132
+ // Note: These may vary by Keycloak version
133
+ const commonProviders = ['login-protocol', 'realm-cache', 'user-storage'];
134
+ const foundProviders = commonProviders.filter(p => providers[p]);
135
+
136
+ // At least some common providers should exist
137
+ expect(foundProviders.length).to.be.greaterThan(0);
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,135 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', 'config');
6
+
7
+ const { conf } = require('propertiesmanager');
8
+ const KeycloakManager = require('../../index');
9
+ const { TEST_REALM, generateUniqueName } = require('../testConfig');
10
+
11
+ const config = {
12
+ baseUrl: conf.keycloak.baseUrl,
13
+ realmName: conf.keycloak.realmName,
14
+ clientId: conf.keycloak.clientId,
15
+ clientSecret: conf.keycloak.clientSecret,
16
+ grantType: conf.keycloak.grantType,
17
+ username: conf.keycloak.username,
18
+ password: conf.keycloak.password,
19
+ tokenLifeSpan: conf.keycloak.tokenLifeSpan
20
+ };
21
+
22
+ /**
23
+ * Integration tests for User Profile Handler
24
+ * Tests user profile configuration and metadata (Keycloak 15+)
25
+ */
26
+ describe('User Profile Handler Tests', function () {
27
+ this.timeout(10000);
28
+
29
+ before(async function () {
30
+ await KeycloakManager.configure(config);
31
+ });
32
+
33
+ after(async function () {
34
+ KeycloakManager.stop();
35
+ });
36
+
37
+ describe('User Profile Configuration', function () {
38
+ let originalConfig;
39
+
40
+ it('should get user profile configuration', async function () {
41
+ try {
42
+ const profileConfig = await KeycloakManager.userProfile.getConfiguration({});
43
+
44
+ expect(profileConfig).to.exist;
45
+ expect(profileConfig).to.have.property('attributes');
46
+ expect(profileConfig.attributes).to.be.an('array');
47
+
48
+ // Save original config for restoration
49
+ originalConfig = profileConfig;
50
+ } catch (error) {
51
+ // User Profile API only available in Keycloak 15+
52
+ if (error.response?.status === 404 || error.message?.includes('getProfile')) {
53
+ this.skip();
54
+ }
55
+ throw error;
56
+ }
57
+ });
58
+
59
+ it('should verify standard attributes exist in profile', async function () {
60
+ if (!originalConfig) this.skip();
61
+
62
+ const attributeNames = originalConfig.attributes.map(attr => attr.name);
63
+
64
+ // Standard attributes that should exist
65
+ expect(attributeNames).to.include('username');
66
+ expect(attributeNames).to.include('email');
67
+ });
68
+
69
+ it('should update user profile configuration', async function () {
70
+ if (!originalConfig) this.skip();
71
+ if (!originalConfig.attributes || !Array.isArray(originalConfig.attributes)) this.skip();
72
+
73
+ // Add a custom attribute to the configuration
74
+ const updatedConfig = JSON.parse(JSON.stringify(originalConfig));
75
+
76
+ // Check if test attribute already exists
77
+ const testAttrIndex = updatedConfig.attributes.findIndex(
78
+ attr => attr.name === 'testCustomAttribute'
79
+ );
80
+
81
+ if (testAttrIndex === -1) {
82
+ // Add new test attribute
83
+ updatedConfig.attributes.push({
84
+ name: 'testCustomAttribute',
85
+ displayName: 'Test Custom Attribute',
86
+ validations: {},
87
+ permissions: {
88
+ view: ['admin', 'user'],
89
+ edit: ['admin']
90
+ },
91
+ multivalued: false
92
+ });
93
+ }
94
+
95
+ try {
96
+ await KeycloakManager.userProfile.updateConfiguration({}, updatedConfig);
97
+
98
+ // Verify the update
99
+ const verifyConfig = await KeycloakManager.userProfile.getConfiguration({});
100
+ const customAttrExists = verifyConfig.attributes.some(
101
+ attr => attr.name === 'testCustomAttribute'
102
+ );
103
+
104
+ expect(customAttrExists).to.be.true;
105
+
106
+ // Restore original configuration
107
+ await KeycloakManager.userProfile.updateConfiguration({}, originalConfig);
108
+ } catch (error) {
109
+ // Restore on error
110
+ if (originalConfig) {
111
+ await KeycloakManager.userProfile.updateConfiguration({}, originalConfig);
112
+ }
113
+ throw error;
114
+ }
115
+ });
116
+ });
117
+
118
+ describe('User Profile Metadata', function () {
119
+ it('should get user profile metadata', async function () {
120
+ try {
121
+ const metadata = await KeycloakManager.userProfile.getMetadata({});
122
+
123
+ expect(metadata).to.exist;
124
+ // Metadata typically contains validators, attribute types, etc.
125
+ // Structure may vary by Keycloak version
126
+ } catch (error) {
127
+ // Metadata endpoint might not be available in all versions
128
+ if (error.response?.status === 404 || error.message?.includes('getProfileMetadata')) {
129
+ this.skip();
130
+ }
131
+ throw error;
132
+ }
133
+ });
134
+ });
135
+ });