@webex/webex-core 3.12.0-next.5 → 3.12.0-next.51

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 (56) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +38 -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/index.js +7 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/interceptors/catalog-url.js +87 -0
  9. package/dist/interceptors/catalog-url.js.map +1 -0
  10. package/dist/interceptors/payload-transformer.js +25 -4
  11. package/dist/interceptors/payload-transformer.js.map +1 -1
  12. package/dist/lib/batcher.js +23 -7
  13. package/dist/lib/batcher.js.map +1 -1
  14. package/dist/lib/credentials/credentials.js +103 -4
  15. package/dist/lib/credentials/credentials.js.map +1 -1
  16. package/dist/lib/credentials/token.js +1 -1
  17. package/dist/lib/domains.js +90 -0
  18. package/dist/lib/domains.js.map +1 -0
  19. package/dist/lib/services/service-catalog.js +108 -18
  20. package/dist/lib/services/service-catalog.js.map +1 -1
  21. package/dist/lib/services/services.js +232 -34
  22. package/dist/lib/services/services.js.map +1 -1
  23. package/dist/lib/services-v2/service-catalog.js +14 -13
  24. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  25. package/dist/lib/services-v2/services-v2.js +241 -37
  26. package/dist/lib/services-v2/services-v2.js.map +1 -1
  27. package/dist/plugins/logger.js +1 -1
  28. package/dist/webex-core.js +11 -3
  29. package/dist/webex-core.js.map +1 -1
  30. package/package.json +13 -13
  31. package/src/config.js +42 -0
  32. package/src/credentials-config.js +13 -0
  33. package/src/index.js +1 -0
  34. package/src/interceptors/catalog-url.js +66 -0
  35. package/src/interceptors/payload-transformer.js +23 -1
  36. package/src/lib/batcher.js +25 -10
  37. package/src/lib/credentials/credentials.js +93 -3
  38. package/src/lib/domains.ts +94 -0
  39. package/src/lib/services/service-catalog.js +112 -16
  40. package/src/lib/services/services.js +208 -20
  41. package/src/lib/services-v2/service-catalog.ts +16 -12
  42. package/src/lib/services-v2/services-v2.ts +218 -21
  43. package/src/webex-core.js +8 -0
  44. package/test/fixtures/activation-email.ts +22 -0
  45. package/test/integration/spec/services/services.js +183 -112
  46. package/test/integration/spec/services-v2/services-v2.js +169 -98
  47. package/test/unit/spec/credentials/credentials.js +247 -3
  48. package/test/unit/spec/interceptors/auth.js +56 -0
  49. package/test/unit/spec/interceptors/catalog-url.js +224 -0
  50. package/test/unit/spec/interceptors/payload-transformer.js +119 -1
  51. package/test/unit/spec/lib/batcher.js +56 -0
  52. package/test/unit/spec/services/service-catalog.js +368 -11
  53. package/test/unit/spec/services/services.js +440 -2
  54. package/test/unit/spec/services-v2/service-catalog.ts +134 -11
  55. package/test/unit/spec/services-v2/services-v2.ts +467 -0
  56. package/test/unit/spec/webex-core.js +77 -1
@@ -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', () => {
@@ -320,11 +321,13 @@ describe('webex-core', () => {
320
321
  services._loadCatalogFromCache = sinon.stub().resolves(false);
321
322
  services.initServiceCatalogs = sinon.stub().resolves();
322
323
  services.initialize();
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');
323
327
  webex.trigger('ready');
324
328
  // Wait for the async 'ready' handler to complete
325
329
  await new Promise((resolve) => setTimeout(resolve, 50));
326
330
  assert.called(services.initServiceCatalogs);
327
- assert.isTrue(catalog.isReady);
328
331
  });
329
332
 
330
333
  it('should collect different catalogs based on OrgId region', () =>
@@ -341,6 +344,45 @@ describe('webex-core', () => {
341
344
  done();
342
345
  }, 2000);
343
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
+ });
344
386
  });
345
387
 
346
388
  describe('#initServiceCatalogs()', () => {
@@ -790,18 +832,22 @@ describe('webex-core', () => {
790
832
  }));
791
833
 
792
834
  it('validates an authorized user and webex instance', () =>
793
- services.validateUser({email: webexUser.email}).then((r) => {
794
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
795
- assert.equal(r.activated, true);
796
- assert.equal(r.exists, true);
797
- }));
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
+ ));
798
842
 
799
843
  it('validates an authorized EU user and webex instance', () =>
800
- servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
801
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
802
- assert.equal(r.activated, true);
803
- assert.equal(r.exists, true);
804
- }));
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
+ ));
805
851
 
806
852
  it("returns a rejected promise if the provided email isn't valid", () =>
807
853
  unauthServices
@@ -814,113 +860,134 @@ describe('webex-core', () => {
814
860
  }));
815
861
 
816
862
  it('validates a non-existing user', () =>
817
- unauthServices
818
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
819
- .then((r) => {
863
+ retryOn429(() =>
864
+ unauthServices.validateUser({email: createActivationEmail()}).then((r) => {
820
865
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
821
866
  assert.equal(r.activated, false);
822
867
  assert.equal(r.exists, false);
823
- }));
868
+ })
869
+ ));
824
870
 
825
871
  it('validates new user with activationOptions suppressEmail false', () =>
826
- unauthServices
827
- .validateUser({
828
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
829
- activationOptions: {suppressEmail: false},
830
- })
831
- .then((r) => {
832
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
833
- assert.equal(r.activated, false);
834
- assert.equal(r.exists, false);
835
- assert.equal(r.user.verificationEmailTriggered, true);
836
- }));
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
+ ));
837
885
 
838
886
  it('validates new user with activationOptions suppressEmail true', () =>
839
- unauthServices
840
- .validateUser({
841
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
842
- activationOptions: {suppressEmail: true},
843
- })
844
- .then((r) => {
845
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
846
- assert.equal(r.activated, false);
847
- assert.equal(r.exists, false);
848
- assert.equal(r.user.verificationEmailTriggered, false);
849
- }));
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
+ ));
850
900
 
851
901
  it('validates an inactive user', () => {
852
902
  const inactive = 'webex.web.client+nonactivated@gmail.com';
853
903
 
854
- return unauthServices
855
- .validateUser({email: inactive, activationOptions: {suppressEmail: true}})
856
- .then((r) => {
857
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
858
- assert.equal(r.activated, false, 'activated');
859
- assert.equal(r.exists, true, 'exists');
860
- })
861
- .catch(() => {
862
- assert(true);
863
- });
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
+ });
864
915
  });
865
916
 
866
917
  it('validates an existing user', () =>
867
- unauthServices.validateUser({email: webexUser.email}).then((r) => {
868
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
869
- assert.equal(r.activated, true);
870
- assert.equal(r.exists, true);
871
- }));
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
+ ));
872
925
 
873
926
  it('validates an existing EU user', () =>
874
- unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
875
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
876
- assert.equal(r.activated, true);
877
- assert.equal(r.exists, true);
878
- }));
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
+ ));
879
934
 
880
935
  it('sends the prelogin user id as undefined when not specified', () => {
881
936
  const requestStub = sandbox.spy(unauthServices, 'request');
882
937
 
883
- return unauthServices
884
- .validateUser({
885
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
886
- activationOptions: {suppressEmail: true},
887
- })
888
- .then(() => {
889
- assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
890
- });
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
+ });
891
950
  });
892
951
 
893
952
  it('sends the prelogin user id as provided when specified', () => {
894
953
  const requestStub = sandbox.spy(unauthServices, 'request');
895
954
  const preloginUserId = uuid.v4();
896
955
 
897
- return unauthServices
898
- .validateUser({
899
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
900
- activationOptions: {suppressEmail: true},
901
- preloginUserId,
902
- })
903
- .then(() => {
904
- assert.strictEqual(
905
- getActivationRequest(requestStub).headers['x-prelogin-userid'],
906
- preloginUserId
907
- );
908
- });
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
+ });
909
972
  });
910
973
 
911
974
  it('uses the license service by default', () => {
912
975
  const requestStub = sandbox.spy(unauthServices, 'request');
913
976
 
914
- return unauthServices
915
- .validateUser({
916
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
917
- activationOptions: {suppressEmail: true},
918
- })
919
- .then(() => {
920
- const request = getActivationRequest(requestStub, false);
921
- assert.strictEqual(request.service, 'license');
922
- assert.strictEqual(request.resource, 'users/activations');
923
- });
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
+ });
924
991
  });
925
992
 
926
993
  it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
@@ -934,16 +1001,20 @@ describe('webex-core', () => {
934
1001
  const userOnboardingServices = userOnboardingWebex.internal.services;
935
1002
  const requestStub = sandbox.spy(userOnboardingServices, 'request');
936
1003
 
937
- return userOnboardingServices
938
- .validateUser({
939
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
940
- activationOptions: {suppressEmail: true},
941
- })
942
- .then(() => {
943
- const request = getActivationRequest(requestStub, true);
944
- assert.strictEqual(request.service, 'user-onboarding');
945
- assert.strictEqual(request.resource, 'api/v1/users/activations');
946
- });
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
+ });
947
1018
  });
948
1019
  });
949
1020
 
@@ -7,7 +7,7 @@ import {assert} from '@webex/test-helper-chai';
7
7
  import sinon from 'sinon';
8
8
  import MockWebex from '@webex/test-helper-mock-webex';
9
9
  import {Credentials, Token, grantErrors} from '@webex/webex-core';
10
- import {inBrowser} from '@webex/common';
10
+ import {inBrowser, base64} from '@webex/common';
11
11
  import FakeTimers from '@sinonjs/fake-timers';
12
12
  import {skipInBrowser} from '@webex/test-helper-mocha';
13
13
  import Logger from '@webex/plugin-logger';
@@ -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,12 +498,96 @@ 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());
375
507
  });
376
508
 
509
+ describe('#extractUserIdFromToken()', () => {
510
+ let credentials;
511
+ let webex;
512
+
513
+ beforeEach(() => {
514
+ webex = new MockWebex();
515
+ credentials = new Credentials(undefined, {parent: webex});
516
+ });
517
+
518
+ const buildToken = (payload) =>
519
+ `header.${base64.toBase64Url(JSON.stringify(payload))}.signature`;
520
+
521
+ it('should return the cis_uuid from the provided token', () => {
522
+ const token = buildToken({cis_uuid: 'my-user-id'});
523
+
524
+ assert.equal(credentials.extractUserIdFromToken(token), 'my-user-id');
525
+ });
526
+
527
+ it('should throw if the token does not contain a cis_uuid', () => {
528
+ const token = buildToken({foo: 'bar'});
529
+
530
+ expect(() => credentials.extractUserIdFromToken(token)).toThrow(
531
+ 'the provided token does not contain a user ID'
532
+ );
533
+ });
534
+
535
+ it('should throw when provided an unparseable token', () =>
536
+ expect(() => credentials.extractUserIdFromToken('not-a-valid-token')).toThrow());
537
+
538
+ it('should throw when no token is provided', () =>
539
+ expect(() => credentials.extractUserIdFromToken()).toThrow());
540
+ });
541
+
542
+ describe('#getUserId()', () => {
543
+ let credentials;
544
+ let webex;
545
+
546
+ const buildToken = (userId) =>
547
+ `header.${base64.toBase64Url(JSON.stringify({cis_uuid: userId}))}.signature`;
548
+
549
+ beforeEach(() => {
550
+ webex = new MockWebex();
551
+ credentials = new Credentials(undefined, {parent: webex});
552
+ });
553
+
554
+ it('should return the userId from the supertoken', () => {
555
+ credentials.supertoken = makeToken(webex, {
556
+ access_token: buildToken('supertoken-user-id'),
557
+ });
558
+
559
+ assert.equal(credentials.getUserId(), 'supertoken-user-id');
560
+ });
561
+
562
+ it('should fall back to a user token when the supertoken has no userId', () => {
563
+ credentials.supertoken = makeToken(webex, {access_token: 'AT'});
564
+ credentials.userTokens.add(
565
+ makeToken(webex, {access_token: buildToken('user-token-user-id'), scope: 'scope1'})
566
+ );
567
+
568
+ assert.equal(credentials.getUserId(), 'user-token-user-id');
569
+ });
570
+
571
+ it('should prefer the supertoken over the user tokens', () => {
572
+ credentials.supertoken = makeToken(webex, {
573
+ access_token: buildToken('supertoken-user-id'),
574
+ });
575
+ credentials.userTokens.add(
576
+ makeToken(webex, {access_token: buildToken('user-token-user-id'), scope: 'scope1'})
577
+ );
578
+
579
+ assert.equal(credentials.getUserId(), 'supertoken-user-id');
580
+ });
581
+
582
+ it('should throw if no available token contains a userId', () => {
583
+ credentials.supertoken = makeToken(webex, {access_token: 'AT'});
584
+
585
+ expect(() => credentials.getUserId()).toThrow(
586
+ 'could not extract the user ID from any available token'
587
+ );
588
+ });
589
+ });
590
+
377
591
  describe('#initialize()', () => {
378
592
  it('turns a portal auth string into a configuration object', () => {
379
593
  const webex = new MockWebex();
@@ -498,6 +712,37 @@ describe('webex-core', () => {
498
712
  });
499
713
  });
500
714
 
715
+ describe('#getClientToken()', () => {
716
+ it('requests a client token without bypassing catalog URL validation', async () => {
717
+ const webex = new MockWebex();
718
+ const credentials = new Credentials(undefined, {parent: webex});
719
+ const uri = 'https://idbroker.webex.com/idb/oauth2/v1/access_token';
720
+ const scope = 'spark:all';
721
+
722
+ webex.request.resolves({body: {access_token: 'AT', token_type: 'Bearer'}});
723
+
724
+ const token = await credentials.getClientToken({uri, scope});
725
+
726
+ assert.calledOnceWithExactly(webex.request, {
727
+ method: 'POST',
728
+ uri,
729
+ form: {
730
+ grant_type: 'client_credentials',
731
+ scope,
732
+ self_contained_token: true,
733
+ },
734
+ auth: {
735
+ user: 'fake',
736
+ pass: 'fake',
737
+ sendImmediately: true,
738
+ },
739
+ shouldRefreshAccessToken: false,
740
+ });
741
+ assert.instanceOf(token, Token);
742
+ assert.equal(token.access_token, 'AT');
743
+ });
744
+ });
745
+
501
746
  describe('#getUserToken()', () => {
502
747
  it('resolves with the supertoken if the supertoken matches the requested scopes', () => {
503
748
  const webex = new MockWebex();
@@ -799,7 +1044,6 @@ describe('webex-core', () => {
799
1044
  .then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
800
1045
  });
801
1046
 
802
-
803
1047
  // it('does not induce any token refreshes');
804
1048
 
805
1049
  it('prevents #getUserToken() from being invoked', () => {