@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-llmrefactor.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.
Files changed (49) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/credentials-config.js +12 -0
  5. package/dist/credentials-config.js.map +1 -1
  6. package/dist/interceptors/redirect.js +1 -1
  7. package/dist/interceptors/redirect.js.map +1 -1
  8. package/dist/lib/batcher.js +23 -7
  9. package/dist/lib/batcher.js.map +1 -1
  10. package/dist/lib/credentials/credentials.js +48 -4
  11. package/dist/lib/credentials/credentials.js.map +1 -1
  12. package/dist/lib/credentials/token.js +1 -1
  13. package/dist/lib/domains.js +90 -0
  14. package/dist/lib/domains.js.map +1 -0
  15. package/dist/lib/services/service-catalog.js +6 -10
  16. package/dist/lib/services/service-catalog.js.map +1 -1
  17. package/dist/lib/services/services.js +208 -51
  18. package/dist/lib/services/services.js.map +1 -1
  19. package/dist/lib/services-v2/service-catalog.js +6 -12
  20. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  21. package/dist/lib/services-v2/services-v2.js +207 -46
  22. package/dist/lib/services-v2/services-v2.js.map +1 -1
  23. package/dist/plugins/logger.js +1 -1
  24. package/dist/webex-core.js +2 -2
  25. package/dist/webex-core.js.map +1 -1
  26. package/package.json +13 -13
  27. package/src/config.js +17 -0
  28. package/src/credentials-config.js +13 -0
  29. package/src/interceptors/redirect.js +4 -1
  30. package/src/lib/batcher.js +25 -10
  31. package/src/lib/credentials/credentials.js +50 -3
  32. package/src/lib/domains.ts +94 -0
  33. package/src/lib/services/service-catalog.js +6 -10
  34. package/src/lib/services/services.js +174 -37
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +173 -33
  37. package/test/fixtures/activation-email.ts +22 -0
  38. package/test/integration/spec/services/service-catalog.js +7 -6
  39. package/test/integration/spec/services/services.js +49 -24
  40. package/test/integration/spec/services-v2/services-v2.js +49 -24
  41. package/test/unit/spec/credentials/credentials.js +133 -2
  42. package/test/unit/spec/interceptors/auth.js +56 -0
  43. package/test/unit/spec/lib/batcher.js +56 -0
  44. package/test/unit/spec/services/service-catalog.js +93 -11
  45. package/test/unit/spec/services/services.js +458 -322
  46. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  47. package/test/unit/spec/services-v2/services-v2.ts +403 -214
  48. package/test/unit/spec/webex-core.js +0 -2
  49. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -20,6 +20,7 @@ import WebexCore, {
20
20
  import testUsers from '@webex/test-helper-test-users';
21
21
  import uuid from 'uuid';
22
22
  import sinon from 'sinon';
23
+ import {createActivationEmail} from '../../../fixtures/activation-email';
23
24
 
24
25
  /* eslint-disable no-underscore-dangle */
25
26
  describe('webex-core', () => {
@@ -404,28 +405,19 @@ describe('webex-core', () => {
404
405
  assert.isTrue(catalog.isReady);
405
406
  });
406
407
 
407
- it('should call services#initServiceCatalogs() on webex loaded', async () => {
408
+ it('should call services#initServiceCatalogs() on webex ready', async () => {
409
+ services._loadCatalogFromCache = sinon.stub().resolves(false);
408
410
  services.initServiceCatalogs = sinon.stub().resolves();
409
411
  services.initialize();
410
- webex.trigger('loaded');
411
- // Wait for the async callback to execute
412
- await new Promise((resolve) => setTimeout(resolve, 10));
412
+ // The mode-specific ('ready'/'loaded') listener is registered inside the
413
+ // change:config handler, so fire change:config first, then 'ready'.
414
+ webex.trigger('change:config');
415
+ webex.trigger('ready');
416
+ // Wait for the async 'ready' handler to complete
417
+ await new Promise((resolve) => setTimeout(resolve, 50));
413
418
  assert.called(services.initServiceCatalogs);
414
419
  });
415
420
 
416
- it('should set services.ready to true after initialization completes', async () => {
417
- // services.ready starts as false
418
- const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
419
- const newServices = newWebex.internal.services;
420
-
421
- // Wait for initialization to complete
422
- await new Promise((resolve) => {
423
- newServices.on('services:initialized', resolve);
424
- });
425
-
426
- assert.isTrue(newServices.ready);
427
- });
428
-
429
421
  it('should collect different catalogs based on OrgId region', () =>
430
422
  assert.notDeepEqual(services.list(true), servicesEU.list(true)));
431
423
 
@@ -441,6 +433,39 @@ describe('webex-core', () => {
441
433
  done();
442
434
  }, 2000);
443
435
  });
436
+
437
+ it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
438
+ const gatedWebex = new WebexCore({
439
+ credentials: {supertoken: webexUser.token},
440
+ config: {services: {waitForCatalogInit: true}},
441
+ });
442
+
443
+ // Before init settles, webex.ready must be false because services.ready
444
+ // is a dependency and starts false in the gated path.
445
+ assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
446
+ assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
447
+
448
+ // Wait up to 30s for services init to complete and flip ready.
449
+ await new Promise((resolve, reject) => {
450
+ if (gatedWebex.internal.services.ready) {
451
+ resolve();
452
+
453
+ return;
454
+ }
455
+ const timer = setTimeout(
456
+ () => reject(new Error('timed out waiting for services.ready')),
457
+ 30_000
458
+ );
459
+
460
+ gatedWebex.internal.services.once('change:ready', () => {
461
+ clearTimeout(timer);
462
+ resolve();
463
+ });
464
+ });
465
+
466
+ assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
467
+ assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
468
+ });
444
469
  });
445
470
 
446
471
  describe('#initServiceCatalogs()', () => {
@@ -903,7 +928,7 @@ describe('webex-core', () => {
903
928
 
904
929
  it('validates a non-existing user', () =>
905
930
  unauthServices
906
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
931
+ .validateUser({email: createActivationEmail()})
907
932
  .then((r) => {
908
933
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
909
934
  assert.equal(r.activated, false);
@@ -916,7 +941,7 @@ describe('webex-core', () => {
916
941
  it('validates new user with activationOptions suppressEmail false', () =>
917
942
  unauthServices
918
943
  .validateUser({
919
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
944
+ email: createActivationEmail(),
920
945
  activationOptions: {suppressEmail: false},
921
946
  })
922
947
  .then((r) => {
@@ -932,7 +957,7 @@ describe('webex-core', () => {
932
957
  it('validates new user with activationOptions suppressEmail true', () =>
933
958
  unauthServices
934
959
  .validateUser({
935
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
960
+ email: createActivationEmail(),
936
961
  activationOptions: {suppressEmail: true},
937
962
  })
938
963
  .then((r) => {
@@ -988,7 +1013,7 @@ describe('webex-core', () => {
988
1013
 
989
1014
  return unauthServices
990
1015
  .validateUser({
991
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1016
+ email: createActivationEmail(),
992
1017
  activationOptions: {suppressEmail: true},
993
1018
  })
994
1019
  .then(() => {
@@ -1002,7 +1027,7 @@ describe('webex-core', () => {
1002
1027
 
1003
1028
  return unauthServices
1004
1029
  .validateUser({
1005
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1030
+ email: createActivationEmail(),
1006
1031
  activationOptions: {suppressEmail: true},
1007
1032
  preloginUserId,
1008
1033
  })
@@ -1019,7 +1044,7 @@ describe('webex-core', () => {
1019
1044
 
1020
1045
  return unauthServices
1021
1046
  .validateUser({
1022
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1047
+ email: createActivationEmail(),
1023
1048
  activationOptions: {suppressEmail: true},
1024
1049
  })
1025
1050
  .then(() => {
@@ -1042,7 +1067,7 @@ describe('webex-core', () => {
1042
1067
 
1043
1068
  return userOnboardingServices
1044
1069
  .validateUser({
1045
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1070
+ email: createActivationEmail(),
1046
1071
  activationOptions: {suppressEmail: true},
1047
1072
  })
1048
1073
  .then(() => {
@@ -25,6 +25,7 @@ import {
25
25
  formattedServiceHostmapEntryTest,
26
26
  serviceHostmapV2,
27
27
  } from '../../../fixtures/host-catalog-v2';
28
+ import {createActivationEmail} from '../../../fixtures/activation-email';
28
29
 
29
30
  // /* eslint-disable no-underscore-dangle */
30
31
  describe('webex-core', () => {
@@ -316,28 +317,19 @@ describe('webex-core', () => {
316
317
  assert.isTrue(catalog.isReady);
317
318
  });
318
319
 
319
- it('should call services#initServiceCatalogs() on webex loaded', async () => {
320
+ it('should call services#initServiceCatalogs() on webex ready', async () => {
321
+ services._loadCatalogFromCache = sinon.stub().resolves(false);
320
322
  services.initServiceCatalogs = sinon.stub().resolves();
321
323
  services.initialize();
322
- webex.trigger('loaded');
323
- // Wait for the async callback to execute
324
- await new Promise((resolve) => setTimeout(resolve, 10));
324
+ // The mode-specific ('ready'/'loaded') listener is registered inside the
325
+ // change:config handler, so fire change:config first, then 'ready'.
326
+ webex.trigger('change:config');
327
+ webex.trigger('ready');
328
+ // Wait for the async 'ready' handler to complete
329
+ await new Promise((resolve) => setTimeout(resolve, 50));
325
330
  assert.called(services.initServiceCatalogs);
326
331
  });
327
332
 
328
- it('should set services.ready to true after initialization completes', async () => {
329
- // services.ready starts as false
330
- const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
331
- const newServices = newWebex.internal.services;
332
-
333
- // Wait for initialization to complete
334
- await new Promise((resolve) => {
335
- newServices.on('services:initialized', resolve);
336
- });
337
-
338
- assert.isTrue(newServices.ready);
339
- });
340
-
341
333
  it('should collect different catalogs based on OrgId region', () =>
342
334
  assert.notDeepEqual(catalog._getAllServiceDetails(), catalogEU._getAllServiceDetails()));
343
335
 
@@ -352,6 +344,39 @@ describe('webex-core', () => {
352
344
  done();
353
345
  }, 2000);
354
346
  });
347
+
348
+ it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
349
+ const gatedWebex = new WebexCore({
350
+ credentials: {supertoken: webexUser.token},
351
+ config: {services: {waitForCatalogInit: true}},
352
+ });
353
+
354
+ // Before init settles, webex.ready must be false because services.ready
355
+ // is a dependency and starts false in the gated path.
356
+ assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
357
+ assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
358
+
359
+ // Wait up to 30s for services init to complete and flip ready.
360
+ await new Promise((resolve, reject) => {
361
+ if (gatedWebex.internal.services.ready) {
362
+ resolve();
363
+
364
+ return;
365
+ }
366
+ const timer = setTimeout(
367
+ () => reject(new Error('timed out waiting for services.ready')),
368
+ 30_000
369
+ );
370
+
371
+ gatedWebex.internal.services.once('change:ready', () => {
372
+ clearTimeout(timer);
373
+ resolve();
374
+ });
375
+ });
376
+
377
+ assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
378
+ assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
379
+ });
355
380
  });
356
381
 
357
382
  describe('#initServiceCatalogs()', () => {
@@ -826,7 +851,7 @@ describe('webex-core', () => {
826
851
 
827
852
  it('validates a non-existing user', () =>
828
853
  unauthServices
829
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
854
+ .validateUser({email: createActivationEmail()})
830
855
  .then((r) => {
831
856
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
832
857
  assert.equal(r.activated, false);
@@ -836,7 +861,7 @@ describe('webex-core', () => {
836
861
  it('validates new user with activationOptions suppressEmail false', () =>
837
862
  unauthServices
838
863
  .validateUser({
839
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
864
+ email: createActivationEmail(),
840
865
  activationOptions: {suppressEmail: false},
841
866
  })
842
867
  .then((r) => {
@@ -849,7 +874,7 @@ describe('webex-core', () => {
849
874
  it('validates new user with activationOptions suppressEmail true', () =>
850
875
  unauthServices
851
876
  .validateUser({
852
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
877
+ email: createActivationEmail(),
853
878
  activationOptions: {suppressEmail: true},
854
879
  })
855
880
  .then((r) => {
@@ -893,7 +918,7 @@ describe('webex-core', () => {
893
918
 
894
919
  return unauthServices
895
920
  .validateUser({
896
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
921
+ email: createActivationEmail(),
897
922
  activationOptions: {suppressEmail: true},
898
923
  })
899
924
  .then(() => {
@@ -907,7 +932,7 @@ describe('webex-core', () => {
907
932
 
908
933
  return unauthServices
909
934
  .validateUser({
910
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
935
+ email: createActivationEmail(),
911
936
  activationOptions: {suppressEmail: true},
912
937
  preloginUserId,
913
938
  })
@@ -924,7 +949,7 @@ describe('webex-core', () => {
924
949
 
925
950
  return unauthServices
926
951
  .validateUser({
927
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
952
+ email: createActivationEmail(),
928
953
  activationOptions: {suppressEmail: true},
929
954
  })
930
955
  .then(() => {
@@ -947,7 +972,7 @@ describe('webex-core', () => {
947
972
 
948
973
  return userOnboardingServices
949
974
  .validateUser({
950
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
975
+ email: createActivationEmail(),
951
976
  activationOptions: {suppressEmail: true},
952
977
  })
953
978
  .then(() => {
@@ -183,6 +183,10 @@ describe('webex-core', () => {
183
183
  webex.credentials.buildLoginUrl({state: 'state'});
184
184
  }, /if specified, `options.state` must be an object/);
185
185
 
186
+ assert.throws(() => {
187
+ webex.credentials.buildLoginUrl({state: null});
188
+ }, /if specified, `options.state` must be an object/);
189
+
186
190
  assert.doesNotThrow(() => {
187
191
  webex.credentials.buildLoginUrl({state: {}});
188
192
  }, /if specified, `options.state` must be an object/);
@@ -232,6 +236,132 @@ describe('webex-core', () => {
232
236
  });
233
237
  });
234
238
 
239
+ describe('#buildThirdPartyLoginUrl()', () => {
240
+ it('throws if both `oauth2provider` and `returnURL` are missing', () => {
241
+ const webex = new MockWebex();
242
+ const credentials = new Credentials(undefined, {parent: webex});
243
+
244
+ webex.trigger('change:config');
245
+
246
+ assert.throws(() => {
247
+ credentials.buildThirdPartyLoginUrl({});
248
+ }, /`options.oauth2provider` is required/);
249
+ });
250
+
251
+ it('throws if `oauth2provider` is missing', () => {
252
+ const webex = new MockWebex();
253
+ const credentials = new Credentials(undefined, {parent: webex});
254
+
255
+ webex.trigger('change:config');
256
+
257
+ assert.throws(() => {
258
+ credentials.buildThirdPartyLoginUrl({returnURL: 'https://web.webex.com'});
259
+ }, /`options.oauth2provider` is required/);
260
+ });
261
+
262
+ it('throws if `returnURL` is missing', () => {
263
+ const webex = new MockWebex();
264
+ const credentials = new Credentials(undefined, {parent: webex});
265
+
266
+ webex.trigger('change:config');
267
+
268
+ assert.throws(() => {
269
+ credentials.buildThirdPartyLoginUrl({oauth2provider: 'google'});
270
+ }, /`options.returnURL` is required/);
271
+ });
272
+
273
+ skipInBrowser(it)('generates the third-party login url', () => {
274
+ const webex = new MockWebex();
275
+ const credentials = new Credentials(undefined, {parent: webex});
276
+
277
+ webex.trigger('change:config');
278
+
279
+ assert.equal(
280
+ credentials.buildThirdPartyLoginUrl({
281
+ oauth2provider: 'google',
282
+ returnURL: 'https://web.webex.com',
283
+ }),
284
+ `${
285
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
286
+ }/idb/ThirdPartyLogin?oauth2provider=google&returnURL=https%3A%2F%2Fweb.webex.com`
287
+ );
288
+ });
289
+
290
+ skipInBrowser(it)('generates the url with different parameter values', () => {
291
+ const webex = new MockWebex();
292
+ const credentials = new Credentials(undefined, {parent: webex});
293
+
294
+ webex.trigger('change:config');
295
+
296
+ assert.equal(
297
+ credentials.buildThirdPartyLoginUrl({
298
+ oauth2provider: 'apple',
299
+ returnURL: 'https://example.com/callback',
300
+ }),
301
+ `${
302
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
303
+ }/idb/ThirdPartyLogin?oauth2provider=apple&returnURL=https%3A%2F%2Fexample.com%2Fcallback`
304
+ );
305
+ });
306
+
307
+ it('throws if `state` is not an object', () => {
308
+ const webex = new MockWebex();
309
+ const credentials = new Credentials(undefined, {parent: webex});
310
+
311
+ webex.trigger('change:config');
312
+
313
+ assert.throws(() => {
314
+ credentials.buildThirdPartyLoginUrl({
315
+ oauth2provider: 'google',
316
+ returnURL: 'https://web.webex.com',
317
+ state: 'not-an-object',
318
+ });
319
+ }, /`options.state` must be an object/);
320
+ });
321
+
322
+ skipInBrowser(it)('omits `state` when an empty object is provided', () => {
323
+ const webex = new MockWebex();
324
+ const credentials = new Credentials(undefined, {parent: webex});
325
+
326
+ webex.trigger('change:config');
327
+
328
+ const result = credentials.buildThirdPartyLoginUrl({
329
+ oauth2provider: 'google',
330
+ returnURL: 'https://web.webex.com',
331
+ state: {},
332
+ });
333
+
334
+ const parsed = new URL(result);
335
+
336
+ assert.isFalse(parsed.searchParams.has('state'));
337
+ });
338
+
339
+ skipInBrowser(it)('base64url-encodes a non-empty `state` and emits it as a top-level query param', () => {
340
+ const webex = new MockWebex();
341
+ const credentials = new Credentials(undefined, {parent: webex});
342
+
343
+ webex.trigger('change:config');
344
+
345
+ const result = credentials.buildThirdPartyLoginUrl({
346
+ oauth2provider: 'google',
347
+ returnURL: 'https://web.webex.com',
348
+ state: {csrf_token: 'abc', popUpSignIn: true},
349
+ });
350
+
351
+ // Literal base64url of '{"csrf_token":"abc","popUpSignIn":true}'
352
+ const expectedState = 'eyJjc3JmX3Rva2VuIjoiYWJjIiwicG9wVXBTaWduSW4iOnRydWV9';
353
+
354
+ assert.equal(
355
+ result,
356
+ `${
357
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
358
+ }/idb/ThirdPartyLogin?oauth2provider=google&returnURL=${encodeURIComponent(
359
+ 'https://web.webex.com'
360
+ )}&state=${expectedState}`
361
+ );
362
+ });
363
+ });
364
+
235
365
  describe('#buildLogoutUrl()', () => {
236
366
  skipInBrowser(it)('generates the logout url', () => {
237
367
  const webex = new MockWebex();
@@ -368,7 +498,9 @@ describe('webex-core', () => {
368
498
  });
369
499
 
370
500
  it('should throw when provided an invalid token', () =>
371
- expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format, token has 1 sections'));
501
+ expect(() => credentials.extractOrgIdFromUserToken()).toThrow(
502
+ 'the provided token is not a valid format, token has 1 sections'
503
+ ));
372
504
 
373
505
  it('should throw when no token is provided', () =>
374
506
  expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
@@ -799,7 +931,6 @@ describe('webex-core', () => {
799
931
  .then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
800
932
  });
801
933
 
802
-
803
934
  // it('does not induce any token refreshes');
804
935
 
805
936
  it('prevents #getUserToken() from being invoked', () => {
@@ -14,6 +14,8 @@ import {
14
14
  AuthInterceptor,
15
15
  config,
16
16
  Credentials,
17
+ Services,
18
+ ServicesV2,
17
19
  WebexHttpError,
18
20
  Token,
19
21
  serviceConstants,
@@ -380,6 +382,60 @@ describe('webex-core', () => {
380
382
  });
381
383
  });
382
384
 
385
+ describe('#onRequest() against a real service catalog', () => {
386
+ // The cases above stub `isAllowedDomainUrl`, so they answer the
387
+ // allowed-domain question themselves and cannot show that the catalog
388
+ // matcher is reached by the code that attaches the token. These use a
389
+ // real catalog and real allowed-domain methods.
390
+ [
391
+ {name: 'Services', Constructor: Services},
392
+ {name: 'ServicesV2', Constructor: ServicesV2},
393
+ ].forEach(({name, Constructor}) => {
394
+ describe(name, () => {
395
+ let getUserToken;
396
+
397
+ beforeEach(() => {
398
+ const services = new Constructor(undefined, {parent: webex});
399
+
400
+ services._getCatalog().setAllowedDomains(['webex.com']);
401
+ // the catalog holds no services, so a url that is not covered by
402
+ // an allowed domain has nothing else to authorize it
403
+ services.waitForService = sinon.stub().rejects(new Error('no such service'));
404
+
405
+ webex.internal.services = services;
406
+ getUserToken = sinon.spy(webex.credentials, 'getUserToken');
407
+ });
408
+
409
+ afterEach(() => {
410
+ getUserToken.restore();
411
+ delete webex.internal.services;
412
+ });
413
+
414
+ it('adds the authorization header for a url under an allowed domain', () =>
415
+ interceptor
416
+ .onRequest({uri: 'https://api.webex.com/resource', headers: {}})
417
+ .then((options) => {
418
+ assert.equal(
419
+ options.headers.authorization,
420
+ webex.credentials.supertoken.toString()
421
+ );
422
+ assert.calledOnce(getUserToken);
423
+ }));
424
+
425
+ [
426
+ 'https://notwebex.com/resource',
427
+ 'https://webex.com.unrelated.example/resource',
428
+ ].forEach((uri) => {
429
+ it(`does not add the authorization header for ${uri}`, () =>
430
+ interceptor.onRequest({uri, headers: {}}).then((options) => {
431
+ assert.notProperty(options.headers, 'authorization');
432
+ assert.notCalled(getUserToken);
433
+ }));
434
+ });
435
+ });
436
+ });
437
+ });
438
+
383
439
  describe('#onResponseError()', () => {
384
440
  describe('when the server responds with 401', () => {
385
441
  nodeOnly(it)('refreshes the access token and replays the request', () => {
@@ -7,6 +7,7 @@ import {assert} from '@webex/test-helper-chai';
7
7
  import MockWebex from '@webex/test-helper-mock-webex';
8
8
  import sinon from 'sinon';
9
9
  import {Batcher} from '@webex/webex-core';
10
+ import WebexHttpError from '../../../../src/lib/webex-http-error';
10
11
 
11
12
  function promiseTick(count) {
12
13
  let promise = Promise.resolve();
@@ -154,6 +155,61 @@ describe('webex-core', () => {
154
155
  return Promise.all([assert.isRejected(p1), assert.isRejected(p2)]);
155
156
  });
156
157
  });
158
+
159
+ it('does not trigger unhandledRejection when caller handles rejection', () => {
160
+ const unhandled = [];
161
+ const onUnhandledRejection = (reason) => {
162
+ unhandled.push(reason);
163
+ };
164
+
165
+ process.on('unhandledRejection', onUnhandledRejection);
166
+
167
+ const p = webex.internal.batcher.request(1);
168
+
169
+ // eslint-disable-next-line prefer-promise-reject-errors
170
+ webex.request.returns(Promise.reject({statusCode: 0}));
171
+
172
+ return promiseTick(50)
173
+ .then(() => clock.tick(2))
174
+ .then(() => promiseTick(50))
175
+ .then(() => assert.isRejected(p))
176
+ .then(() => promiseTick(50))
177
+ .then(() => {
178
+ assert.lengthOf(unhandled, 0);
179
+ })
180
+ .finally(() => {
181
+ process.removeListener('unhandledRejection', onUnhandledRejection);
182
+ });
183
+ });
184
+
185
+ it('fails queued deferreds for webex http errors without request body', () => {
186
+ const p1 = webex.internal.batcher.request(1);
187
+ const p2 = webex.internal.batcher.request(2);
188
+ const reason = new WebexHttpError.BadRequest({
189
+ statusCode: 400,
190
+ body: {message: 'simulated failure'},
191
+ options: {
192
+ method: 'GET',
193
+ uri: 'https://example.com/v1/mock/batch',
194
+ headers: {trackingid: 'test-tracking-id'},
195
+ },
196
+ headers: {},
197
+ });
198
+
199
+ webex.request.returns(Promise.reject(reason));
200
+
201
+ return promiseTick(50)
202
+ .then(() => clock.tick(2))
203
+ .then(() => promiseTick(50))
204
+ .then(() => {
205
+ assert.calledOnce(webex.request);
206
+
207
+ return Promise.all([
208
+ assert.isRejected(p1, /simulated failure/),
209
+ assert.isRejected(p2, /simulated failure/),
210
+ ]);
211
+ });
212
+ });
157
213
  });
158
214
 
159
215
  describe('when the number of request attempts exceeds a given threshold', () => {