@unito/integration-cli 0.63.2 → 0.63.4

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.
@@ -102,7 +102,7 @@ class Oauth2 extends core_1.Command {
102
102
  }
103
103
  const refreshToken = decryptionResult?.successful.refreshToken;
104
104
  core_1.ux.action.start(`Refreshing test account ${testAccount}`, undefined, { stdout: true });
105
- credentials = await Oauth2Resource.updateToken(oauth2, refreshToken);
105
+ credentials = await Oauth2Resource.updateToken(oauth2, refreshToken, decryptionResult?.successful, environment);
106
106
  // If provider response doesn't contain a new refresh token, use the existing one
107
107
  credentials.refreshToken = credentials.refreshToken ?? refreshToken;
108
108
  core_1.ux.action.stop();
@@ -1,4 +1,4 @@
1
1
  import { Oauth2Response, Oauth2Payload } from '../services/oauth2';
2
2
  import { Environment } from './globalConfiguration';
3
3
  export declare function performOAuth2Flow(applicationCredentials: Oauth2Payload, environment?: Environment, credentialPayload?: Record<string, unknown>): Promise<Oauth2Response>;
4
- export declare function updateToken(applicationCredentials: Oauth2Payload, refreshToken: string): Promise<Oauth2Response>;
4
+ export declare function updateToken(applicationCredentials: Oauth2Payload, refreshToken: string, credentialPayload?: Record<string, unknown>, environment?: Environment): Promise<Oauth2Response>;
@@ -22,7 +22,7 @@ async function performOAuth2Flow(applicationCredentials, environment = globalCon
22
22
  }
23
23
  return oauth2Response;
24
24
  }
25
- async function updateToken(applicationCredentials, refreshToken) {
26
- const oauthHelper = new oauth2_1.default(applicationCredentials);
25
+ async function updateToken(applicationCredentials, refreshToken, credentialPayload, environment = globalConfiguration_1.Environment.Production) {
26
+ const oauthHelper = new oauth2_1.default(applicationCredentials, environment, credentialPayload);
27
27
  return oauthHelper.updateToken(refreshToken);
28
28
  }
@@ -110,6 +110,8 @@ class OAuth2Service {
110
110
  }
111
111
  }
112
112
  const templateVariables = { ...(this.credentialPayload ?? {}), ...authorizationResponseVariables };
113
+ templateVariables.clientId ??= this.clientId;
114
+ templateVariables.clientSecret ??= this.clientSecret;
113
115
  const tokenRequestPayload = {
114
116
  ...Object.fromEntries(Object.entries(this.tokenRequestParameters?.body ?? {}).map(([key, value]) => [
115
117
  key,
@@ -235,11 +237,23 @@ class OAuth2Service {
235
237
  bodyData.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
236
238
  bodyData.redirect_uri = `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback-cli`;
237
239
  }
240
+ const templateVariables = structuredClone(this.credentialPayload ?? {});
241
+ templateVariables.clientId ??= this.clientId;
242
+ templateVariables.clientSecret ??= this.clientSecret;
243
+ const tokenRequestHeaders = {
244
+ 'Content-Type': this.requestContentType,
245
+ ...(this.refreshRequestParameters?.header ?? {}),
246
+ };
247
+ for (const [headerKey, headerValue] of Object.entries(this.tokenRequestParameters?.header ?? {})) {
248
+ tokenRequestHeaders[headerKey] = (0, template_1.expandTemplate)(String(headerValue), templateVariables, {
249
+ urlEncodeVariables: false,
250
+ });
251
+ }
238
252
  try {
239
- const tokenResponse = await fetch(this.tokenUrl, {
253
+ const tokenResponse = await fetch((0, template_1.expandTemplate)(this.tokenUrl, templateVariables, { urlEncodeVariables: true }), {
240
254
  headers: {
241
255
  'Content-Type': this.requestContentType,
242
- ...(this.refreshRequestParameters?.header ?? {}),
256
+ ...(tokenRequestHeaders ?? {}),
243
257
  },
244
258
  body: this.encodeBody(bodyData, this.requestContentType),
245
259
  method: 'POST',
@@ -303,4 +303,70 @@ describe('oauth2', () => {
303
303
  .it('must successfully decrypt all secrets', ctx => {
304
304
  (0, test_1.expect)(ctx.stderr).to.contain((0, styles_1.uncolorize)('The secret boom! could not be decrypted.'));
305
305
  });
306
+ test_1.test
307
+ .stdout()
308
+ .do(() => {
309
+ getConfigurationsStub.returns({
310
+ ...baseConfiguration,
311
+ testAccounts: {
312
+ development: credentials,
313
+ },
314
+ });
315
+ sinon
316
+ .stub(IntegrationsPlatform, 'encryptData')
317
+ .onFirstCall()
318
+ .resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedAccessToken` })
319
+ .onSecondCall()
320
+ .resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedRefreshToken` });
321
+ })
322
+ .stub(DecryptionResource, 'decryptEntries', stub => stub
323
+ .onFirstCall()
324
+ .resolves({ successful: { ...oauth2Information }, failed: [] })
325
+ .onSecondCall()
326
+ .resolves({ successful: { refreshToken: 'test-refresh-token' }, failed: [] }))
327
+ .command(['oauth2', '--test-account', 'development', '--environment', 'staging'])
328
+ .it('passes the staging environment to updateToken when specified', () => {
329
+ (0, test_1.expect)(performOAuth2FlowStub.getCalls().length).to.equal(0);
330
+ (0, test_1.expect)(updateTokenStub.getCalls().length).to.equal(1);
331
+ // Verify that updateToken was called with the correct environment parameter
332
+ const updateTokenCall = updateTokenStub.getCall(0);
333
+ (0, test_1.expect)(updateTokenCall.args).to.have.lengthOf(4);
334
+ (0, test_1.expect)(updateTokenCall.args[0]).to.deep.equal(oauth2Information); // oauth2 config
335
+ (0, test_1.expect)(updateTokenCall.args[1]).to.equal('test-refresh-token'); // refresh token
336
+ (0, test_1.expect)(updateTokenCall.args[2]).to.deep.equal({ refreshToken: 'test-refresh-token' }); // credential payload
337
+ (0, test_1.expect)(updateTokenCall.args[3]).to.equal('staging'); // environment
338
+ });
339
+ test_1.test
340
+ .stdout()
341
+ .do(() => {
342
+ getConfigurationsStub.returns({
343
+ ...baseConfiguration,
344
+ testAccounts: {
345
+ development: credentials,
346
+ },
347
+ });
348
+ sinon
349
+ .stub(IntegrationsPlatform, 'encryptData')
350
+ .onFirstCall()
351
+ .resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedAccessToken` })
352
+ .onSecondCall()
353
+ .resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedRefreshToken` });
354
+ })
355
+ .stub(DecryptionResource, 'decryptEntries', stub => stub
356
+ .onFirstCall()
357
+ .resolves({ successful: { ...oauth2Information }, failed: [] })
358
+ .onSecondCall()
359
+ .resolves({ successful: { refreshToken: 'test-refresh-token' }, failed: [] }))
360
+ .command(['oauth2', '--test-account', 'development'])
361
+ .it('defaults to production environment when no environment flag is specified', () => {
362
+ (0, test_1.expect)(performOAuth2FlowStub.getCalls().length).to.equal(0);
363
+ (0, test_1.expect)(updateTokenStub.getCalls().length).to.equal(1);
364
+ // Verify that updateToken was called with the default production environment
365
+ const updateTokenCall = updateTokenStub.getCall(0);
366
+ (0, test_1.expect)(updateTokenCall.args).to.have.lengthOf(4);
367
+ (0, test_1.expect)(updateTokenCall.args[0]).to.deep.equal(oauth2Information); // oauth2 config
368
+ (0, test_1.expect)(updateTokenCall.args[1]).to.equal('test-refresh-token'); // refresh token
369
+ (0, test_1.expect)(updateTokenCall.args[2]).to.deep.equal({ refreshToken: 'test-refresh-token' }); // credential payload
370
+ (0, test_1.expect)(updateTokenCall.args[3]).to.equal('production'); // environment (default)
371
+ });
306
372
  });
@@ -52,4 +52,32 @@ describe('OAuth2Helper', () => {
52
52
  await strict_1.default.rejects(response, errors_1.FailedToRetrieveAccessTokenError);
53
53
  });
54
54
  });
55
+ describe('updateToken', () => {
56
+ let updateTokenStub;
57
+ beforeEach(() => {
58
+ updateTokenStub = sinon_1.default.stub(oauth2_1.default.prototype, 'updateToken');
59
+ });
60
+ it('should create OAuth2Service with credential payload and call updateToken', async () => {
61
+ const mockResponse = { accessToken: 'new-token', refreshToken: 'new-refresh-token' };
62
+ updateTokenStub.resolves(mockResponse);
63
+ const credentialPayload = { domain: 'test-domain', customVar: 'test-value' };
64
+ const refreshToken = 'test-refresh-token';
65
+ const result = await oauth2HelperResource.updateToken(authorizationInfo, refreshToken, credentialPayload);
66
+ // Assert that the OAuth2Service was instantiated with the credential payload
67
+ // and that updateToken was called with the refresh token
68
+ sinon_1.default.assert.calledOnce(updateTokenStub);
69
+ sinon_1.default.assert.calledWith(updateTokenStub, refreshToken);
70
+ strict_1.default.deepEqual(result, mockResponse);
71
+ });
72
+ it('should work without credential payload', async () => {
73
+ const mockResponse = { accessToken: 'new-token', refreshToken: 'new-refresh-token' };
74
+ updateTokenStub.resolves(mockResponse);
75
+ const refreshToken = 'test-refresh-token';
76
+ const result = await oauth2HelperResource.updateToken(authorizationInfo, refreshToken);
77
+ // Should still work when no credential payload is provided
78
+ sinon_1.default.assert.calledOnce(updateTokenStub);
79
+ sinon_1.default.assert.calledWith(updateTokenStub, refreshToken);
80
+ strict_1.default.deepEqual(result, mockResponse);
81
+ });
82
+ });
55
83
  });
@@ -142,7 +142,121 @@ describe('OAuth2Helper', () => {
142
142
  sinon_1.default.assert.calledOnce(res.send);
143
143
  sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG());
144
144
  });
145
- it('handles form-urlencoded error responses', async () => {
145
+ describe('template variables for clientId and clientSecret', () => {
146
+ const setupHelperWithStubs = (authorizationInfo, credentialPayload) => {
147
+ oauth2Helper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production, credentialPayload);
148
+ sinon_1.default.stub(oauth2Helper, 'startServer').resolves('http://localhost:5050');
149
+ sinon_1.default.stub(oauth2Helper, 'stopServer');
150
+ };
151
+ afterEach(() => {
152
+ sinon_1.default.restore();
153
+ });
154
+ const performCallback = async () => {
155
+ const req = { query: { code: 'test-code' } };
156
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
157
+ await oauth2Helper['handleCallback'](req, res);
158
+ return { req, res };
159
+ };
160
+ it('expands clientId and clientSecret in headers', async () => {
161
+ // What matters: Template variables {+clientId} and {+clientSecret} in headers
162
+ // should be replaced with actual service-level client credentials
163
+ setupHelperWithStubs({
164
+ clientId: 'my-client-id',
165
+ clientSecret: 'my-client-secret',
166
+ authorizationUrl: 'https://provider.com/oauth/authorize',
167
+ scopes: [{ name: 'scope1' }],
168
+ tokenUrl: 'https://provider.com/oauth/token',
169
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
170
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
171
+ tokenRequestParameters: {
172
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
173
+ },
174
+ });
175
+ await performCallback();
176
+ // What matters: The fetch call should have expanded the template variables in headers
177
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
178
+ headers: {
179
+ 'Content-Type': 'application/x-www-form-urlencoded',
180
+ 'X-Client-ID': 'my-client-id', // Template {+clientId} expanded
181
+ 'X-Client-Secret': 'my-client-secret', // Template {+clientSecret} expanded
182
+ },
183
+ body: sinon_1.default.match.string,
184
+ method: 'POST',
185
+ });
186
+ });
187
+ it('expands clientId and clientSecret in body', async () => {
188
+ // What matters: Template variables in request body should be expanded
189
+ // while preserving other static parameters
190
+ setupHelperWithStubs({
191
+ clientId: 'my-client-id',
192
+ clientSecret: 'my-client-secret',
193
+ authorizationUrl: 'https://provider.com/oauth/authorize',
194
+ scopes: [{ name: 'scope1' }],
195
+ tokenUrl: 'https://provider.com/oauth/token',
196
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
197
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
198
+ tokenRequestParameters: {
199
+ body: {
200
+ custom_client_id: '{+clientId}',
201
+ custom_client_secret: '{+clientSecret}',
202
+ additional_param: 'static_value',
203
+ },
204
+ },
205
+ });
206
+ await performCallback();
207
+ const requestBody = fetchStub.getCall(0).args[1].body;
208
+ const bodyParams = new URLSearchParams(requestBody);
209
+ // What matters: Template variables in body are expanded to actual values
210
+ strict_1.default.equal(bodyParams.get('custom_client_id'), 'my-client-id');
211
+ strict_1.default.equal(bodyParams.get('custom_client_secret'), 'my-client-secret');
212
+ // What matters: Static parameters are preserved unchanged
213
+ strict_1.default.equal(bodyParams.get('additional_param'), 'static_value');
214
+ });
215
+ it('expands clientId and clientSecret in URL', async () => {
216
+ // What matters: Template variables in token URL should be expanded
217
+ // and properly URL-encoded as query parameters
218
+ setupHelperWithStubs({
219
+ clientId: 'my-client-id',
220
+ clientSecret: 'my-client-secret',
221
+ authorizationUrl: 'https://provider.com/oauth/authorize',
222
+ scopes: [{ name: 'scope1' }],
223
+ tokenUrl: 'https://provider.com/oauth/token?client_id={+clientId}&secret={+clientSecret}',
224
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
225
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
226
+ });
227
+ await performCallback();
228
+ // What matters: The URL template variables are expanded in the fetch call
229
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token?client_id=my-client-id&secret=my-client-secret', sinon_1.default.match.object);
230
+ });
231
+ it('allows credential payload to override service values', async () => {
232
+ // What matters: Credential payload values should take precedence over
233
+ // service-level clientId/clientSecret when expanding templates
234
+ setupHelperWithStubs({
235
+ clientId: 'default-client-id', // Service-level values
236
+ clientSecret: 'default-client-secret',
237
+ authorizationUrl: 'https://provider.com/oauth/authorize',
238
+ scopes: [{ name: 'scope1' }],
239
+ tokenUrl: 'https://provider.com/oauth/token',
240
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
241
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
242
+ tokenRequestParameters: {
243
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
244
+ },
245
+ }, { clientId: 'override-client-id', clientSecret: 'override-client-secret' });
246
+ await performCallback();
247
+ // What matters: Template expansion uses the credential payload override values
248
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
249
+ headers: {
250
+ 'Content-Type': 'application/x-www-form-urlencoded',
251
+ 'X-Client-ID': 'override-client-id', // Uses credential payload value
252
+ 'X-Client-Secret': 'override-client-secret', // Uses credential payload value
253
+ },
254
+ body: sinon_1.default.match.string,
255
+ method: 'POST',
256
+ });
257
+ });
258
+ });
259
+ it('form-urlencoded error responses', async () => {
146
260
  const code = 'test-code';
147
261
  const req = { query: { code } };
148
262
  const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
@@ -187,9 +301,9 @@ describe('OAuth2Helper', () => {
187
301
  });
188
302
  });
189
303
  describe('updateToken', () => {
190
- it('retrieves the access token when a refresh token is available', async () => {
304
+ const setupSuccessfulRefreshResponse = () => {
191
305
  const refreshToken = 'test-refresh-token';
192
- const accessToken = 'test-access-token';
306
+ const accessToken = 'new-access-token';
193
307
  const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
194
308
  fetchStub.resolves({
195
309
  status: 200,
@@ -197,9 +311,103 @@ describe('OAuth2Helper', () => {
197
311
  text: sinon_1.default.stub().resolves(''),
198
312
  headers: new Headers({ 'content-type': 'application/json' }),
199
313
  });
314
+ return { refreshToken, accessToken };
315
+ };
316
+ it('retrieves the access token when a refresh token is available', async () => {
317
+ const { refreshToken, accessToken } = setupSuccessfulRefreshResponse();
200
318
  const result = await oauth2Helper.updateToken(refreshToken);
201
319
  strict_1.default.deepEqual(result, { accessToken, refreshToken });
202
320
  sinon_1.default.assert.calledOnce(fetchStub);
321
+ // Assert that the refresh token is actually sent in the request body
322
+ const fetchCall = fetchStub.getCall(0);
323
+ const requestBody = fetchCall.args[1].body;
324
+ const bodyParams = new URLSearchParams(requestBody);
325
+ strict_1.default.equal(bodyParams.get('refresh_token'), refreshToken);
326
+ strict_1.default.equal(bodyParams.get('grant_type'), 'refresh_token');
327
+ });
328
+ describe('template variables for clientId and clientSecret', () => {
329
+ const createHelperWithTemplateHeaders = (clientId, clientSecret, credentialPayload) => {
330
+ return new oauth2_1.default({
331
+ clientId,
332
+ clientSecret,
333
+ authorizationUrl: 'https://provider.com/oauth/authorize',
334
+ scopes: [{ name: 'scope1' }],
335
+ tokenUrl: 'https://provider.com/oauth/token',
336
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
337
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
338
+ tokenRequestParameters: {
339
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
340
+ },
341
+ }, globalConfiguration_1.Environment.Production, credentialPayload);
342
+ };
343
+ it('expands template variables in headers', async () => {
344
+ // What matters: Template variables in tokenRequestParameters headers
345
+ // should be expanded during token refresh operations
346
+ oauth2Helper = createHelperWithTemplateHeaders('refresh-client-id', 'refresh-client-secret');
347
+ const { refreshToken } = setupSuccessfulRefreshResponse();
348
+ await oauth2Helper.updateToken(refreshToken);
349
+ // What matters: Fetch call for refresh token includes expanded template variables
350
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
351
+ headers: {
352
+ 'Content-Type': 'application/x-www-form-urlencoded',
353
+ 'X-Client-ID': 'refresh-client-id', // Template expanded
354
+ 'X-Client-Secret': 'refresh-client-secret', // Template expanded
355
+ },
356
+ body: sinon_1.default.match.string,
357
+ method: 'POST',
358
+ });
359
+ });
360
+ it('allows credential payload override', async () => {
361
+ // What matters: During token refresh, credential payload values should
362
+ // override service-level values for template expansion
363
+ oauth2Helper = createHelperWithTemplateHeaders('default-id', 'default-secret', {
364
+ clientId: 'override-refresh-client-id', // Override values
365
+ clientSecret: 'override-refresh-client-secret',
366
+ });
367
+ const { refreshToken } = setupSuccessfulRefreshResponse();
368
+ await oauth2Helper.updateToken(refreshToken);
369
+ // What matters: Template expansion uses credential payload override values for refresh
370
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
371
+ headers: {
372
+ 'Content-Type': 'application/x-www-form-urlencoded',
373
+ 'X-Client-ID': 'override-refresh-client-id', // Uses override value
374
+ 'X-Client-Secret': 'override-refresh-client-secret', // Uses override value
375
+ },
376
+ body: sinon_1.default.match.string,
377
+ method: 'POST',
378
+ });
379
+ });
380
+ });
381
+ it('includes refreshRequestParameters headers in token request', async () => {
382
+ oauth2Helper = new oauth2_1.default({
383
+ clientId: 'test-client-id',
384
+ clientSecret: 'test-client-secret',
385
+ authorizationUrl: 'https://provider.com/oauth/authorize',
386
+ scopes: [{ name: 'scope1' }],
387
+ tokenUrl: 'https://provider.com/oauth/token',
388
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
389
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
390
+ tokenRequestParameters: {
391
+ header: { 'X-Token-Header': 'token-value' },
392
+ },
393
+ refreshRequestParameters: {
394
+ header: { 'X-Refresh-Header': 'refresh-value', 'X-Special-Auth': 'special-token' },
395
+ },
396
+ }, globalConfiguration_1.Environment.Production);
397
+ const { refreshToken } = setupSuccessfulRefreshResponse();
398
+ await oauth2Helper.updateToken(refreshToken);
399
+ // What matters: Both tokenRequestParameters and refreshRequestParameters headers
400
+ // should be present in the token refresh request
401
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
402
+ headers: {
403
+ 'Content-Type': 'application/x-www-form-urlencoded',
404
+ 'X-Token-Header': 'token-value', // From tokenRequestParameters.header
405
+ 'X-Refresh-Header': 'refresh-value', // From refreshRequestParameters.header
406
+ 'X-Special-Auth': 'special-token', // From refreshRequestParameters.header
407
+ },
408
+ body: sinon_1.default.match.string,
409
+ method: 'POST',
410
+ });
203
411
  });
204
412
  it('throws an error when failing to refresh the access token', async () => {
205
413
  const refreshToken = 'test-refresh-token';
@@ -214,6 +422,43 @@ describe('OAuth2Helper', () => {
214
422
  const response = oauth2Helper.updateToken(refreshToken);
215
423
  await strict_1.default.rejects(response, errors_1.InvalidRequestContentTypeError);
216
424
  });
425
+ describe('token URL template expansion', () => {
426
+ it('expands template variables in token URL during refresh', async () => {
427
+ const credentialPayload = { domain: 'test-domain' };
428
+ oauth2Helper = new oauth2_1.default({
429
+ clientId: 'test-client-id',
430
+ clientSecret: 'test-client-secret',
431
+ authorizationUrl: 'https://provider.com/oauth/authorize',
432
+ scopes: [{ name: 'scope1' }],
433
+ tokenUrl: 'https://{+domain}.provider.com/oauth/token', // Template URL
434
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
435
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
436
+ }, globalConfiguration_1.Environment.Production, credentialPayload);
437
+ const { refreshToken } = setupSuccessfulRefreshResponse();
438
+ await oauth2Helper.updateToken(refreshToken);
439
+ // Assert that the token URL was expanded with the domain
440
+ sinon_1.default.assert.calledWith(fetchStub, 'https://test-domain.provider.com/oauth/token', sinon_1.default.match.object);
441
+ });
442
+ it('expands multiple template variables in token URL', async () => {
443
+ const credentialPayload = {
444
+ domain: 'my-company',
445
+ environment: 'staging',
446
+ };
447
+ oauth2Helper = new oauth2_1.default({
448
+ clientId: 'test-client-id',
449
+ clientSecret: 'test-client-secret',
450
+ authorizationUrl: 'https://provider.com/oauth/authorize',
451
+ scopes: [{ name: 'scope1' }],
452
+ tokenUrl: 'https://{+domain}.{+environment}.provider.com/oauth/token',
453
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
454
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
455
+ }, globalConfiguration_1.Environment.Production, credentialPayload);
456
+ const { refreshToken } = setupSuccessfulRefreshResponse();
457
+ await oauth2Helper.updateToken(refreshToken);
458
+ // Assert that both template variables were expanded
459
+ sinon_1.default.assert.calledWith(fetchStub, 'https://my-company.staging.provider.com/oauth/token', sinon_1.default.match.object);
460
+ });
461
+ });
217
462
  });
218
463
  describe('encodeBody', () => {
219
464
  it('encodes body data as URL-encoded when content type is URL_ENCODED', () => {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.63.2",
2
+ "version": "0.63.4",
3
3
  "commands": {
4
4
  "activity": {
5
5
  "id": "activity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-cli",
3
- "version": "0.63.2",
3
+ "version": "0.63.4",
4
4
  "description": "Integration CLI",
5
5
  "bin": {
6
6
  "integration-cli": "./bin/run"