keycloak-api-manager 3.1.0 → 3.2.1

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.
@@ -0,0 +1,284 @@
1
+ const { expect } = require('chai');
2
+ const { getAdminClient } = require('./config');
3
+
4
+ describe('Clients Handler', function () {
5
+ this.timeout(15000);
6
+ let client;
7
+ let testClientId;
8
+ let testRoleId;
9
+
10
+ before(async function () {
11
+ client = getAdminClient();
12
+
13
+ // Create a test role for client role mappings
14
+ const role = await client.roles.create(
15
+ { realm: 'test-realm' },
16
+ { name: 'clients-test-role' }
17
+ );
18
+ testRoleId = role.id;
19
+ });
20
+
21
+ // ==================== CLIENT CRUD ====================
22
+ describe('CRUD Operations', function () {
23
+ describe('create', function () {
24
+ it('should create a client with valid representation', async function () {
25
+ const clientRep = {
26
+ clientId: `test-client-${Date.now()}`,
27
+ name: 'Integration Test Client',
28
+ enabled: true,
29
+ publicClient: false,
30
+ standardFlowEnabled: true,
31
+ directAccessGrantsEnabled: true,
32
+ };
33
+
34
+ const result = await client.clients.create(
35
+ { realm: 'test-realm' },
36
+ clientRep
37
+ );
38
+
39
+ expect(result).to.have.property('id');
40
+ testClientId = result.id;
41
+ });
42
+
43
+ it('should fail creating duplicate clientId', async function () {
44
+ const clientId = `unique-client-${Date.now()}`;
45
+
46
+ await client.clients.create({ realm: 'test-realm' }, {
47
+ clientId,
48
+ enabled: true,
49
+ });
50
+
51
+ try {
52
+ await client.clients.create({ realm: 'test-realm' }, {
53
+ clientId,
54
+ enabled: true,
55
+ });
56
+ expect.fail('Should have thrown error for duplicate clientId');
57
+ } catch (err) {
58
+ expect(err).to.exist;
59
+ }
60
+ });
61
+ });
62
+
63
+ describe('find', function () {
64
+ it('should list all clients in realm', async function () {
65
+ const clients = await client.clients.find({ realm: 'test-realm' });
66
+
67
+ expect(clients).to.be.an('array');
68
+ expect(clients.length).to.be.greaterThan(0);
69
+ });
70
+
71
+ it('should search clients by clientId', async function () {
72
+ const foundClients = await client.clients.find({
73
+ realm: 'test-realm',
74
+ clientId: `test-client-${Date.now()}`,
75
+ });
76
+
77
+ expect(foundClients).to.be.an('array');
78
+ });
79
+ });
80
+
81
+ describe('findOne', function () {
82
+ it('should find a specific client by id', async function () {
83
+ const foundClient = await client.clients.findOne({
84
+ realm: 'test-realm',
85
+ id: testClientId,
86
+ });
87
+
88
+ expect(foundClient).to.exist;
89
+ expect(foundClient.id).to.equal(testClientId);
90
+ });
91
+ });
92
+
93
+ describe('update', function () {
94
+ it('should update client attributes', async function () {
95
+ const updateRep = {
96
+ name: 'Updated Client Name',
97
+ description: 'Updated description',
98
+ publicClient: true,
99
+ attributes: {
100
+ 'custom-attr': 'value',
101
+ },
102
+ };
103
+
104
+ await client.clients.update(
105
+ { realm: 'test-realm', id: testClientId },
106
+ updateRep
107
+ );
108
+
109
+ const updated = await client.clients.findOne({
110
+ realm: 'test-realm',
111
+ id: testClientId,
112
+ });
113
+
114
+ expect(updated.name).to.equal('Updated Client Name');
115
+ expect(updated.description).to.equal('Updated description');
116
+ });
117
+ });
118
+ });
119
+
120
+ // ==================== CLIENT SECRETS ====================
121
+ describe('Secret Management', function () {
122
+ describe('generateNewClientSecret', function () {
123
+ it('should generate a new client secret', async function () {
124
+ const secret = await client.clients.generateNewClientSecret({
125
+ realm: 'test-realm',
126
+ id: testClientId,
127
+ });
128
+
129
+ expect(secret).to.have.property('value');
130
+ expect(secret.value).to.be.a('string');
131
+ expect(secret.value.length).to.be.greaterThan(0);
132
+ });
133
+ });
134
+
135
+ describe('getClientSecret', function () {
136
+ it('should retrieve the current client secret', async function () {
137
+ const secret = await client.clients.getClientSecret({
138
+ realm: 'test-realm',
139
+ id: testClientId,
140
+ });
141
+
142
+ expect(secret).to.have.property('value');
143
+ expect(secret.value).to.be.a('string');
144
+ });
145
+ });
146
+ });
147
+
148
+ // ==================== CLIENT CREDENTIALS ====================
149
+ describe('Credential Management', function () {
150
+ describe('listClientCredentials', function () {
151
+ it('should list all client credentials', async function () {
152
+ const creds = await client.clients.getCredentials({
153
+ realm: 'test-realm',
154
+ id: testClientId,
155
+ });
156
+
157
+ expect(creds).to.be.an('array');
158
+ });
159
+ });
160
+ });
161
+
162
+ // ==================== CLIENT SESSIONS ====================
163
+ describe('Session Management', function () {
164
+ describe('getUserConsents', function () {
165
+ it('should retrieve user consents for client', async function () {
166
+ // This might be empty but should not error
167
+ try {
168
+ const consents = await client.clients.getUserConsents({
169
+ realm: 'test-realm',
170
+ id: testClientId,
171
+ });
172
+
173
+ expect(consents).to.be.an('array');
174
+ } catch (err) {
175
+ // Some versions might not support this
176
+ }
177
+ });
178
+ });
179
+
180
+ describe('getActiveSessions', function () {
181
+ it('should retrieve active sessions for client', async function () {
182
+ const sessions = await client.clients.getActiveSessions({
183
+ realm: 'test-realm',
184
+ id: testClientId,
185
+ });
186
+
187
+ expect(sessions).to.be.an('array');
188
+ });
189
+ });
190
+
191
+ describe('getOfflineSessions', function () {
192
+ it('should retrieve offline sessions for client', async function () {
193
+ const sessions = await client.clients.getOfflineSessions({
194
+ realm: 'test-realm',
195
+ id: testClientId,
196
+ });
197
+
198
+ expect(sessions).to.be.an('array');
199
+ });
200
+ });
201
+ });
202
+
203
+ // ==================== CLIENT ROLE MAPPINGS ====================
204
+ describe('Role Mappings', function () {
205
+ describe('listServiceAccountRoleMappings', function () {
206
+ it('should list service account role mappings', async function () {
207
+ try {
208
+ const mappings = await client.clients.listServiceAccountRoleMappings({
209
+ realm: 'test-realm',
210
+ id: testClientId,
211
+ });
212
+
213
+ expect(mappings).to.be.an('object');
214
+ } catch (err) {
215
+ // Service account might not be enabled
216
+ }
217
+ });
218
+ });
219
+ });
220
+
221
+ // ==================== PROTOCOL MAPPERS ====================
222
+ describe('Protocol Mappers', function () {
223
+ describe('listProtocolMappers', function () {
224
+ it('should list protocol mappers', async function () {
225
+ const mappers = await client.clients.listProtocolMappers({
226
+ realm: 'test-realm',
227
+ id: testClientId,
228
+ });
229
+
230
+ expect(mappers).to.be.an('array');
231
+ });
232
+ });
233
+
234
+ describe('addProtocolMapper', function () {
235
+ it('should add a protocol mapper', async function () {
236
+ const mapper = {
237
+ name: 'test-mapper',
238
+ protocol: 'openid-connect',
239
+ protocolMapper: 'oidc-usermodel-attribute-mapper',
240
+ config: {
241
+ 'user.attribute': 'email',
242
+ 'claim.name': 'email',
243
+ 'jsonType.label': 'String',
244
+ },
245
+ };
246
+
247
+ const result = await client.clients.addProtocolMapper(
248
+ { realm: 'test-realm', id: testClientId },
249
+ mapper
250
+ );
251
+
252
+ expect(result).to.exist;
253
+ });
254
+ });
255
+ });
256
+
257
+ // ==================== SCOPE MAPPINGS ====================
258
+ describe('Scope Mappings', function () {
259
+ describe('listScopeMappings', function () {
260
+ it('should list all scope mappings', async function () {
261
+ const mappings = await client.clients.listScopeMappings({
262
+ realm: 'test-realm',
263
+ id: testClientId,
264
+ });
265
+
266
+ expect(mappings).to.be.an('object');
267
+ });
268
+ });
269
+ });
270
+
271
+ // ==================== CLEANUP ====================
272
+ after(async function () {
273
+ try {
274
+ if (testClientId) {
275
+ await client.clients.del({ realm: 'test-realm', id: testClientId });
276
+ }
277
+ if (testRoleId) {
278
+ await client.roles.del({ realm: 'test-realm', id: testRoleId });
279
+ }
280
+ } catch (err) {
281
+ console.error('Cleanup error:', err.message);
282
+ }
283
+ });
284
+ });
@@ -0,0 +1,122 @@
1
+ const { expect } = require('chai');
2
+ const { getAdminClient } = require('./config');
3
+
4
+ describe('Components Handler', function () {
5
+ this.timeout(15000);
6
+ let client;
7
+ let testComponentId;
8
+
9
+ before(function () {
10
+ client = getAdminClient();
11
+ });
12
+
13
+ // ==================== COMPONENT CRUD ====================
14
+ describe('CRUD Operations', function () {
15
+ describe('create', function () {
16
+ it('should create a component with valid representation', async function () {
17
+ const componentRep = {
18
+ name: `test-component-${Date.now()}`,
19
+ providerId: 'org.keycloak.storage.UserStorageProvider',
20
+ providerType: 'org.keycloak.storage.UserStorageProvider',
21
+ config: {
22
+ priority: ['0'],
23
+ },
24
+ };
25
+
26
+ const result = await client.components.create(
27
+ { realm: 'test-realm' },
28
+ componentRep
29
+ );
30
+
31
+ expect(result).to.have.property('id');
32
+ testComponentId = result.id;
33
+ });
34
+ });
35
+
36
+ describe('find', function () {
37
+ it('should list all components', async function () {
38
+ const components = await client.components.find({ realm: 'test-realm' });
39
+
40
+ expect(components).to.be.an('array');
41
+ });
42
+
43
+ it('should filter components by type', async function () {
44
+ const components = await client.components.find({
45
+ realm: 'test-realm',
46
+ type: 'org.keycloak.storage.UserStorageProvider',
47
+ });
48
+
49
+ expect(components).to.be.an('array');
50
+ });
51
+
52
+ it('should filter components by provider id', async function () {
53
+ const components = await client.components.find({
54
+ realm: 'test-realm',
55
+ filter: 'org.keycloak.storage.UserStorageProvider',
56
+ });
57
+
58
+ expect(components).to.be.an('array');
59
+ });
60
+ });
61
+
62
+ describe('findOne', function () {
63
+ it('should find a specific component by id', async function () {
64
+ const component = await client.components.findOne({
65
+ realm: 'test-realm',
66
+ id: testComponentId,
67
+ });
68
+
69
+ expect(component).to.exist;
70
+ expect(component.id).to.equal(testComponentId);
71
+ });
72
+ });
73
+
74
+ describe('update', function () {
75
+ it('should update component attributes', async function () {
76
+ const updateRep = {
77
+ name: `updated-component-${Date.now()}`,
78
+ config: {
79
+ priority: ['1'],
80
+ },
81
+ };
82
+
83
+ await client.components.update(
84
+ { realm: 'test-realm', id: testComponentId },
85
+ updateRep
86
+ );
87
+
88
+ const updated = await client.components.findOne({
89
+ realm: 'test-realm',
90
+ id: testComponentId,
91
+ });
92
+
93
+ expect(updated.name).to.include('updated-component');
94
+ });
95
+ });
96
+ });
97
+
98
+ // ==================== SUBCOMPONENTS ====================
99
+ describe('Subcomponents', function () {
100
+ describe('listSubComponents', function () {
101
+ it('should list subcomponents', async function () {
102
+ const subcomponents = await client.components.listSubComponents({
103
+ realm: 'test-realm',
104
+ id: testComponentId,
105
+ });
106
+
107
+ expect(subcomponents).to.be.an('array');
108
+ });
109
+ });
110
+ });
111
+
112
+ // ==================== CLEANUP ====================
113
+ after(async function () {
114
+ try {
115
+ if (testComponentId) {
116
+ await client.components.del({ realm: 'test-realm', id: testComponentId });
117
+ }
118
+ } catch (err) {
119
+ console.error('Cleanup error:', err.message);
120
+ }
121
+ });
122
+ });
package/test/config.js ADDED
@@ -0,0 +1,137 @@
1
+ const KcAdmClient = require('@keycloak/keycloak-admin-client').default;
2
+ const { delay } = require('async');
3
+
4
+ const TEST_CONFIG = {
5
+ baseUrl: process.env.KEYCLOAK_URL || 'http://localhost:8080',
6
+ realmName: 'test-realm',
7
+ username: 'admin',
8
+ password: 'admin',
9
+ clientId: 'admin-cli',
10
+ grantType: 'password',
11
+ };
12
+
13
+ let adminClient = null;
14
+
15
+ /**
16
+ * Inizializza il client Keycloak admin
17
+ * Aspetta che Keycloak sia pronto prima di connettersi
18
+ */
19
+ async function initializeAdminClient() {
20
+ if (adminClient) {
21
+ return adminClient;
22
+ }
23
+
24
+ let retries = 30;
25
+ let lastError;
26
+
27
+ while (retries > 0) {
28
+ try {
29
+ adminClient = new KcAdmClient({
30
+ baseUrl: TEST_CONFIG.baseUrl,
31
+ realmName: TEST_CONFIG.realmName,
32
+ });
33
+
34
+ await adminClient.auth({
35
+ username: TEST_CONFIG.username,
36
+ password: TEST_CONFIG.password,
37
+ clientId: TEST_CONFIG.clientId,
38
+ grantType: TEST_CONFIG.grantType,
39
+ });
40
+
41
+ console.log('✓ Keycloak admin client initialized');
42
+ return adminClient;
43
+ } catch (err) {
44
+ lastError = err;
45
+ retries--;
46
+ if (retries > 0) {
47
+ console.log(`Waiting for Keycloak... (${retries} retries left)`);
48
+ await delay(2000);
49
+ }
50
+ }
51
+ }
52
+
53
+ throw new Error(`Failed to connect to Keycloak after retries: ${lastError.message}`);
54
+ }
55
+
56
+ /**
57
+ * Crea il realm di test
58
+ */
59
+ async function setupTestRealm() {
60
+ const client = await initializeAdminClient();
61
+
62
+ // Switcha a master realm per creare il test realm
63
+ client.realmName = 'master';
64
+
65
+ try {
66
+ // Controlla se il realm esiste già
67
+ const realms = await client.realms.find();
68
+ const realmExists = realms.some((r) => r.realm === TEST_CONFIG.realmName);
69
+
70
+ if (!realmExists) {
71
+ await client.realms.create({
72
+ realm: TEST_CONFIG.realmName,
73
+ displayName: 'Test Realm',
74
+ enabled: true,
75
+ accessTokenLifespan: 3600,
76
+ refreshTokenMaxReuse: 0,
77
+ actionTokenGeneratedByAdminLifespan: 900,
78
+ actionTokenGeneratedByUserLifespan: 900,
79
+ });
80
+ console.log(`✓ Test realm '${TEST_CONFIG.realmName}' created`);
81
+ } else {
82
+ console.log(`✓ Test realm '${TEST_CONFIG.realmName}' already exists`);
83
+ }
84
+ } catch (err) {
85
+ if (err.response?.status === 409) {
86
+ console.log(`✓ Test realm '${TEST_CONFIG.realmName}' already exists`);
87
+ } else {
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ // Switcha back al test realm
93
+ client.realmName = TEST_CONFIG.realmName;
94
+ }
95
+
96
+ /**
97
+ * Pulisce il realm di test
98
+ */
99
+ async function cleanupTestRealm() {
100
+ if (!adminClient) return;
101
+
102
+ try {
103
+ adminClient.realmName = 'master';
104
+ await adminClient.realms.del({ realm: TEST_CONFIG.realmName });
105
+ console.log(`✓ Test realm '${TEST_CONFIG.realmName}' deleted`);
106
+ } catch (err) {
107
+ if (err.response?.status !== 404) {
108
+ console.warn(`Warning: Failed to delete test realm: ${err.message}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Ritorna il client admin configurato e autenticato
115
+ */
116
+ function getAdminClient() {
117
+ if (!adminClient) {
118
+ throw new Error('Admin client not initialized. Call initializeAdminClient() first');
119
+ }
120
+ return adminClient;
121
+ }
122
+
123
+ /**
124
+ * Reset del client (principalmente per i test)
125
+ */
126
+ function resetAdminClient() {
127
+ adminClient = null;
128
+ }
129
+
130
+ module.exports = {
131
+ TEST_CONFIG,
132
+ initializeAdminClient,
133
+ setupTestRealm,
134
+ cleanupTestRealm,
135
+ getAdminClient,
136
+ resetAdminClient,
137
+ };
@@ -0,0 +1,111 @@
1
+ const docker = require('dockerode');
2
+ const { spawn } = require('child_process');
3
+ const { delay } = require('async');
4
+
5
+ /**
6
+ * Starts Docker Compose services
7
+ */
8
+ async function startDocker() {
9
+ console.log('Starting Docker Compose services...');
10
+
11
+ return new Promise((resolve, reject) => {
12
+ // Try docker compose (modern) or docker-compose (legacy)
13
+ const command = 'docker';
14
+ const args = ['compose', 'up', '-d'];
15
+
16
+ const compose = spawn(command, args, {
17
+ cwd: process.cwd(),
18
+ stdio: 'inherit',
19
+ });
20
+
21
+ compose.on('close', (code) => {
22
+ if (code !== 0) {
23
+ reject(new Error(`docker compose up failed with code ${code}`));
24
+ } else {
25
+ console.log('✓ Docker Compose services started');
26
+ resolve();
27
+ }
28
+ });
29
+
30
+ compose.on('error', reject);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Stops Docker Compose services
36
+ */
37
+ async function stopDocker() {
38
+ console.log('Stopping Docker Compose services...');
39
+
40
+ return new Promise((resolve, reject) => {
41
+ const command = 'docker';
42
+ const args = ['compose', 'down', '--volumes'];
43
+
44
+ const compose = spawn(command, args, {
45
+ cwd: process.cwd(),
46
+ stdio: 'inherit',
47
+ });
48
+
49
+ compose.on('close', (code) => {
50
+ if (code !== 0) {
51
+ reject(new Error(`docker compose down failed with code ${code}`));
52
+ } else {
53
+ console.log('✓ Docker Compose services stopped');
54
+ resolve();
55
+ }
56
+ });
57
+
58
+ compose.on('error', reject);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Waits for a service to be healthy
64
+ */
65
+ async function waitForHealthy(maxRetries = 30, delayMs = 2000) {
66
+ let retries = maxRetries;
67
+
68
+ while (retries > 0) {
69
+ try {
70
+ const dockerode = new docker();
71
+ const containers = await dockerode.listContainers();
72
+ const keycloakContainer = containers.find((c) => c.Names.includes('/keycloak-test'));
73
+
74
+ if (!keycloakContainer) {
75
+ throw new Error('Keycloak container not found');
76
+ }
77
+
78
+ const container = dockerode.getContainer(keycloakContainer.Id);
79
+ const inspect = await container.inspect();
80
+
81
+ if (inspect.State.Health?.Status === 'healthy') {
82
+ console.log('✓ Keycloak container is healthy');
83
+ return;
84
+ }
85
+
86
+ retries--;
87
+ if (retries > 0) {
88
+ console.log(
89
+ `Waiting for Keycloak to be healthy (${inspect.State.Health?.Status || 'unknown'})... (${retries} retries left)`
90
+ );
91
+ await delay(delayMs);
92
+ }
93
+ } catch (err) {
94
+ retries--;
95
+ if (retries > 0) {
96
+ console.log(`Waiting for services... (${retries} retries left)`);
97
+ await delay(delayMs);
98
+ } else {
99
+ throw err;
100
+ }
101
+ }
102
+ }
103
+
104
+ throw new Error('Service failed to become healthy in time');
105
+ }
106
+
107
+ module.exports = {
108
+ startDocker,
109
+ stopDocker,
110
+ waitForHealthy,
111
+ };