@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-auth-prejoin-fetch.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 +24 -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 +244 -57
  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 +243 -52
  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 +27 -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 +218 -46
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +218 -42
  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 +188 -128
  40. package/test/integration/spec/services-v2/services-v2.js +174 -114
  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 +591 -322
  46. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  47. package/test/unit/spec/services-v2/services-v2.ts +539 -214
  48. package/test/unit/spec/webex-core.js +0 -2
  49. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -5,7 +5,7 @@
5
5
  import '@webex/internal-plugin-device';
6
6
 
7
7
  import {assert} from '@webex/test-helper-chai';
8
- import {flaky} from '@webex/test-helper-mocha';
8
+ import {flaky, retryOn429} from '@webex/test-helper-mocha';
9
9
  import WebexCore, {
10
10
  ServiceCatalogV2,
11
11
  ServiceDetail,
@@ -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,45 @@ 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(
358
+ gatedWebex.ready,
359
+ 'webex.ready should not fire while services.ready is false'
360
+ );
361
+
362
+ // Wait up to 30s for services init to complete and flip ready.
363
+ await new Promise((resolve, reject) => {
364
+ if (gatedWebex.internal.services.ready) {
365
+ resolve();
366
+
367
+ return;
368
+ }
369
+ const timer = setTimeout(
370
+ () => reject(new Error('timed out waiting for services.ready')),
371
+ 30_000
372
+ );
373
+
374
+ gatedWebex.internal.services.once('change:ready', () => {
375
+ clearTimeout(timer);
376
+ resolve();
377
+ });
378
+ });
379
+
380
+ assert.isTrue(
381
+ gatedWebex.internal.services.ready,
382
+ 'services.ready should flip true after init settles'
383
+ );
384
+ assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
385
+ });
355
386
  });
356
387
 
357
388
  describe('#initServiceCatalogs()', () => {
@@ -801,18 +832,22 @@ describe('webex-core', () => {
801
832
  }));
802
833
 
803
834
  it('validates an authorized user and webex instance', () =>
804
- services.validateUser({email: webexUser.email}).then((r) => {
805
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
806
- assert.equal(r.activated, true);
807
- assert.equal(r.exists, true);
808
- }));
835
+ retryOn429(() =>
836
+ services.validateUser({email: webexUser.email}).then((r) => {
837
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
838
+ assert.equal(r.activated, true);
839
+ assert.equal(r.exists, true);
840
+ })
841
+ ));
809
842
 
810
843
  it('validates an authorized EU user and webex instance', () =>
811
- servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
812
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
813
- assert.equal(r.activated, true);
814
- assert.equal(r.exists, true);
815
- }));
844
+ retryOn429(() =>
845
+ servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
846
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
847
+ assert.equal(r.activated, true);
848
+ assert.equal(r.exists, true);
849
+ })
850
+ ));
816
851
 
817
852
  it("returns a rejected promise if the provided email isn't valid", () =>
818
853
  unauthServices
@@ -825,113 +860,134 @@ describe('webex-core', () => {
825
860
  }));
826
861
 
827
862
  it('validates a non-existing user', () =>
828
- unauthServices
829
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
830
- .then((r) => {
863
+ retryOn429(() =>
864
+ unauthServices.validateUser({email: createActivationEmail()}).then((r) => {
831
865
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
832
866
  assert.equal(r.activated, false);
833
867
  assert.equal(r.exists, false);
834
- }));
868
+ })
869
+ ));
835
870
 
836
871
  it('validates new user with activationOptions suppressEmail false', () =>
837
- unauthServices
838
- .validateUser({
839
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
840
- activationOptions: {suppressEmail: false},
841
- })
842
- .then((r) => {
843
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
844
- assert.equal(r.activated, false);
845
- assert.equal(r.exists, false);
846
- assert.equal(r.user.verificationEmailTriggered, true);
847
- }));
872
+ retryOn429(() =>
873
+ unauthServices
874
+ .validateUser({
875
+ email: createActivationEmail(),
876
+ activationOptions: {suppressEmail: false},
877
+ })
878
+ .then((r) => {
879
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
880
+ assert.equal(r.activated, false);
881
+ assert.equal(r.exists, false);
882
+ assert.equal(r.user.verificationEmailTriggered, true);
883
+ })
884
+ ));
848
885
 
849
886
  it('validates new user with activationOptions suppressEmail true', () =>
850
- unauthServices
851
- .validateUser({
852
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
853
- activationOptions: {suppressEmail: true},
854
- })
855
- .then((r) => {
856
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
857
- assert.equal(r.activated, false);
858
- assert.equal(r.exists, false);
859
- assert.equal(r.user.verificationEmailTriggered, false);
860
- }));
887
+ retryOn429(() =>
888
+ unauthServices
889
+ .validateUser({
890
+ email: createActivationEmail(),
891
+ activationOptions: {suppressEmail: true},
892
+ })
893
+ .then((r) => {
894
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
895
+ assert.equal(r.activated, false);
896
+ assert.equal(r.exists, false);
897
+ assert.equal(r.user.verificationEmailTriggered, false);
898
+ })
899
+ ));
861
900
 
862
901
  it('validates an inactive user', () => {
863
902
  const inactive = 'webex.web.client+nonactivated@gmail.com';
864
903
 
865
- return unauthServices
866
- .validateUser({email: inactive, activationOptions: {suppressEmail: true}})
867
- .then((r) => {
868
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
869
- assert.equal(r.activated, false, 'activated');
870
- assert.equal(r.exists, true, 'exists');
871
- })
872
- .catch(() => {
873
- assert(true);
874
- });
904
+ return retryOn429(() =>
905
+ unauthServices
906
+ .validateUser({email: inactive, activationOptions: {suppressEmail: true}})
907
+ .then((r) => {
908
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
909
+ assert.equal(r.activated, false, 'activated');
910
+ assert.equal(r.exists, true, 'exists');
911
+ })
912
+ ).catch(() => {
913
+ assert(true);
914
+ });
875
915
  });
876
916
 
877
917
  it('validates an existing user', () =>
878
- unauthServices.validateUser({email: webexUser.email}).then((r) => {
879
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
880
- assert.equal(r.activated, true);
881
- assert.equal(r.exists, true);
882
- }));
918
+ retryOn429(() =>
919
+ unauthServices.validateUser({email: webexUser.email}).then((r) => {
920
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
921
+ assert.equal(r.activated, true);
922
+ assert.equal(r.exists, true);
923
+ })
924
+ ));
883
925
 
884
926
  it('validates an existing EU user', () =>
885
- unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
886
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
887
- assert.equal(r.activated, true);
888
- assert.equal(r.exists, true);
889
- }));
927
+ retryOn429(() =>
928
+ unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
929
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
930
+ assert.equal(r.activated, true);
931
+ assert.equal(r.exists, true);
932
+ })
933
+ ));
890
934
 
891
935
  it('sends the prelogin user id as undefined when not specified', () => {
892
936
  const requestStub = sandbox.spy(unauthServices, 'request');
893
937
 
894
- return unauthServices
895
- .validateUser({
896
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
897
- activationOptions: {suppressEmail: true},
898
- })
899
- .then(() => {
900
- assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
901
- });
938
+ return retryOn429(() => {
939
+ requestStub.resetHistory();
940
+
941
+ return unauthServices
942
+ .validateUser({
943
+ email: createActivationEmail(),
944
+ activationOptions: {suppressEmail: true},
945
+ })
946
+ .then(() => {
947
+ assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
948
+ });
949
+ });
902
950
  });
903
951
 
904
952
  it('sends the prelogin user id as provided when specified', () => {
905
953
  const requestStub = sandbox.spy(unauthServices, 'request');
906
954
  const preloginUserId = uuid.v4();
907
955
 
908
- return unauthServices
909
- .validateUser({
910
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
911
- activationOptions: {suppressEmail: true},
912
- preloginUserId,
913
- })
914
- .then(() => {
915
- assert.strictEqual(
916
- getActivationRequest(requestStub).headers['x-prelogin-userid'],
917
- preloginUserId
918
- );
919
- });
956
+ return retryOn429(() => {
957
+ requestStub.resetHistory();
958
+
959
+ return unauthServices
960
+ .validateUser({
961
+ email: createActivationEmail(),
962
+ activationOptions: {suppressEmail: true},
963
+ preloginUserId,
964
+ })
965
+ .then(() => {
966
+ assert.strictEqual(
967
+ getActivationRequest(requestStub).headers['x-prelogin-userid'],
968
+ preloginUserId
969
+ );
970
+ });
971
+ });
920
972
  });
921
973
 
922
974
  it('uses the license service by default', () => {
923
975
  const requestStub = sandbox.spy(unauthServices, 'request');
924
976
 
925
- return unauthServices
926
- .validateUser({
927
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
928
- activationOptions: {suppressEmail: true},
929
- })
930
- .then(() => {
931
- const request = getActivationRequest(requestStub, false);
932
- assert.strictEqual(request.service, 'license');
933
- assert.strictEqual(request.resource, 'users/activations');
934
- });
977
+ return retryOn429(() => {
978
+ requestStub.resetHistory();
979
+
980
+ return unauthServices
981
+ .validateUser({
982
+ email: createActivationEmail(),
983
+ activationOptions: {suppressEmail: true},
984
+ })
985
+ .then(() => {
986
+ const request = getActivationRequest(requestStub, false);
987
+ assert.strictEqual(request.service, 'license');
988
+ assert.strictEqual(request.resource, 'users/activations');
989
+ });
990
+ });
935
991
  });
936
992
 
937
993
  it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
@@ -945,16 +1001,20 @@ describe('webex-core', () => {
945
1001
  const userOnboardingServices = userOnboardingWebex.internal.services;
946
1002
  const requestStub = sandbox.spy(userOnboardingServices, 'request');
947
1003
 
948
- return userOnboardingServices
949
- .validateUser({
950
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
951
- activationOptions: {suppressEmail: true},
952
- })
953
- .then(() => {
954
- const request = getActivationRequest(requestStub, true);
955
- assert.strictEqual(request.service, 'user-onboarding');
956
- assert.strictEqual(request.resource, 'api/v1/users/activations');
957
- });
1004
+ return retryOn429(() => {
1005
+ requestStub.resetHistory();
1006
+
1007
+ return userOnboardingServices
1008
+ .validateUser({
1009
+ email: createActivationEmail(),
1010
+ activationOptions: {suppressEmail: true},
1011
+ })
1012
+ .then(() => {
1013
+ const request = getActivationRequest(requestStub, true);
1014
+ assert.strictEqual(request.service, 'user-onboarding');
1015
+ assert.strictEqual(request.resource, 'api/v1/users/activations');
1016
+ });
1017
+ });
958
1018
  });
959
1019
  });
960
1020
 
@@ -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', () => {