@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.
- package/README.md +5 -2
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/domains.js +90 -0
- package/dist/lib/domains.js.map +1 -0
- package/dist/lib/services/service-catalog.js +6 -10
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +208 -51
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +6 -12
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +207 -46
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +17 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +6 -10
- package/src/lib/services/services.js +174 -37
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +173 -33
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/service-catalog.js +7 -6
- package/test/integration/spec/services/services.js +49 -24
- package/test/integration/spec/services-v2/services-v2.js +49 -24
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +93 -11
- package/test/unit/spec/services/services.js +458 -322
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +403 -214
- package/test/unit/spec/webex-core.js +0 -2
- 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
|
|
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
|
-
|
|
411
|
-
//
|
|
412
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
323
|
-
//
|
|
324
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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', () => {
|