@webex/webex-core 3.12.0-next.9 → 3.12.0-webex-services-ready.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/dist/credentials-config.js +12 -0
- package/dist/credentials-config.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/services/services.js +109 -29
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +108 -26
- 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/credentials-config.js +13 -0
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/services/services.js +86 -12
- package/src/lib/services-v2/services-v2.ts +85 -10
- package/test/integration/spec/services/service-catalog.js +6 -7
- package/test/integration/spec/services/services.js +17 -6
- package/test/integration/spec/services-v2/services-v2.js +17 -6
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/services.js +342 -122
- package/test/unit/spec/services-v2/services-v2.ts +236 -63
- package/test/unit/spec/webex-core.js +2 -0
- package/test/unit/spec/webex-internal-core.js +2 -0
|
@@ -32,6 +32,12 @@ const DEFAULT_CLUSTER_IDENTIFIER =
|
|
|
32
32
|
const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
|
|
33
33
|
const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
34
34
|
|
|
35
|
+
// Maximum time we will wait for the initial catalog collection before letting
|
|
36
|
+
// `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
|
|
37
|
+
// must never leave the app on a permanent spinner - past this point downstream
|
|
38
|
+
// consumers must fall through to their normal error/login paths.
|
|
39
|
+
const SERVICES_INIT_TIMEOUT_MS = 15_000;
|
|
40
|
+
|
|
35
41
|
/* eslint-disable no-underscore-dangle */
|
|
36
42
|
/**
|
|
37
43
|
* @class
|
|
@@ -44,6 +50,20 @@ const Services = WebexPlugin.extend({
|
|
|
44
50
|
initFailed: ['boolean', false, false],
|
|
45
51
|
},
|
|
46
52
|
|
|
53
|
+
session: {
|
|
54
|
+
/**
|
|
55
|
+
* Becomes `true` once services initialization has completed.
|
|
56
|
+
* This blocks `webex.ready` until services are initialized.
|
|
57
|
+
* @instance
|
|
58
|
+
* @memberof Services
|
|
59
|
+
* @type {boolean}
|
|
60
|
+
*/
|
|
61
|
+
ready: {
|
|
62
|
+
default: false,
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
47
67
|
_catalogs: new WeakMap(),
|
|
48
68
|
|
|
49
69
|
_activeServices: {},
|
|
@@ -1337,6 +1357,30 @@ const Services = WebexPlugin.extend({
|
|
|
1337
1357
|
);
|
|
1338
1358
|
},
|
|
1339
1359
|
|
|
1360
|
+
/**
|
|
1361
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1362
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1363
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1364
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1365
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1366
|
+
* under them.
|
|
1367
|
+
*
|
|
1368
|
+
* @private
|
|
1369
|
+
* @returns {Promise<void>}
|
|
1370
|
+
*/
|
|
1371
|
+
async _finalizeReady(): Promise<void> {
|
|
1372
|
+
const {credentials} = this.webex;
|
|
1373
|
+
|
|
1374
|
+
if (credentials && credentials.isRefreshing) {
|
|
1375
|
+
await new Promise<void>((resolve) => {
|
|
1376
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
this.ready = true;
|
|
1381
|
+
this.trigger('services:initialized');
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1340
1384
|
/**
|
|
1341
1385
|
* Initializer
|
|
1342
1386
|
*
|
|
@@ -1353,19 +1397,32 @@ const Services = WebexPlugin.extend({
|
|
|
1353
1397
|
this.initConfig();
|
|
1354
1398
|
});
|
|
1355
1399
|
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1400
|
+
// Wait for storage to be loaded before attempting to update the service catalogs.
|
|
1401
|
+
// We listen for 'loaded' instead of 'ready' because services.ready is a dependency
|
|
1402
|
+
// of webex.ready - listening to 'ready' would cause a deadlock.
|
|
1403
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1359
1404
|
const warmed = await this._loadCatalogFromCache();
|
|
1360
1405
|
if (warmed) {
|
|
1361
1406
|
catalog.isReady = true;
|
|
1407
|
+
await this._finalizeReady();
|
|
1362
1408
|
|
|
1363
1409
|
return;
|
|
1364
1410
|
}
|
|
1365
1411
|
const {supertoken} = this.webex.credentials;
|
|
1412
|
+
|
|
1413
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1414
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1415
|
+
// leave the app on a permanent spinner.
|
|
1416
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
1417
|
+
setTimeout(
|
|
1418
|
+
() => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
|
|
1419
|
+
SERVICES_INIT_TIMEOUT_MS
|
|
1420
|
+
);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1366
1423
|
// Validate if the supertoken exists.
|
|
1367
1424
|
if (supertoken && supertoken.access_token) {
|
|
1368
|
-
this.initServiceCatalogs()
|
|
1425
|
+
Promise.race([this.initServiceCatalogs(), timeout])
|
|
1369
1426
|
.then(() => {
|
|
1370
1427
|
catalog.isReady = true;
|
|
1371
1428
|
})
|
|
@@ -1374,15 +1431,33 @@ const Services = WebexPlugin.extend({
|
|
|
1374
1431
|
this.logger.error(
|
|
1375
1432
|
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1376
1433
|
);
|
|
1377
|
-
})
|
|
1434
|
+
})
|
|
1435
|
+
.finally(() => this._finalizeReady());
|
|
1378
1436
|
} else {
|
|
1379
1437
|
const {email} = this.webex.config;
|
|
1380
1438
|
|
|
1381
|
-
this.collectPreauthCatalog(email ? {email} : undefined)
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1439
|
+
Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
|
|
1440
|
+
.catch((error) => {
|
|
1441
|
+
this.initFailed = true;
|
|
1442
|
+
this.logger.error(
|
|
1443
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1444
|
+
);
|
|
1445
|
+
})
|
|
1446
|
+
.finally(() => this._finalizeReady());
|
|
1447
|
+
// Listen for when credentials become available to fetch the full catalog.
|
|
1448
|
+
// This handles fresh login where 'loaded' fires before OAuth completes.
|
|
1449
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1450
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1451
|
+
this.initServiceCatalogs()
|
|
1452
|
+
.then(() => {
|
|
1453
|
+
catalog.isReady = true;
|
|
1454
|
+
})
|
|
1455
|
+
.catch((error) => {
|
|
1456
|
+
this.logger.error(
|
|
1457
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1386
1461
|
});
|
|
1387
1462
|
}
|
|
1388
1463
|
});
|
|
@@ -23,13 +23,12 @@ describe('webex-core', () => {
|
|
|
23
23
|
.then(
|
|
24
24
|
([user]) =>
|
|
25
25
|
new Promise((resolve) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}, 1000);
|
|
26
|
+
webexUser = user;
|
|
27
|
+
webex = new WebexCore({credentials: user.token});
|
|
28
|
+
services = webex.internal.services;
|
|
29
|
+
catalog = services._getCatalog();
|
|
30
|
+
// Wait for webex ready event before registering device to ensure newMetrics.callDiagnosticMetrics is initialized
|
|
31
|
+
webex.once('ready', resolve);
|
|
33
32
|
})
|
|
34
33
|
)
|
|
35
34
|
.then(() => webex.internal.device.register())
|
|
@@ -404,15 +404,26 @@ describe('webex-core', () => {
|
|
|
404
404
|
assert.isTrue(catalog.isReady);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
it('should call services#initServiceCatalogs() on webex
|
|
408
|
-
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
407
|
+
it('should call services#initServiceCatalogs() on webex loaded', async () => {
|
|
409
408
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
410
409
|
services.initialize();
|
|
411
|
-
webex.trigger('
|
|
412
|
-
// Wait for the async
|
|
413
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
410
|
+
webex.trigger('loaded');
|
|
411
|
+
// Wait for the async callback to execute
|
|
412
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
414
413
|
assert.called(services.initServiceCatalogs);
|
|
415
|
-
|
|
414
|
+
});
|
|
415
|
+
|
|
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);
|
|
416
427
|
});
|
|
417
428
|
|
|
418
429
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -316,15 +316,26 @@ describe('webex-core', () => {
|
|
|
316
316
|
assert.isTrue(catalog.isReady);
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
it('should call services#initServiceCatalogs() on webex
|
|
320
|
-
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
319
|
+
it('should call services#initServiceCatalogs() on webex loaded', async () => {
|
|
321
320
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
322
321
|
services.initialize();
|
|
323
|
-
webex.trigger('
|
|
324
|
-
// Wait for the async
|
|
325
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
322
|
+
webex.trigger('loaded');
|
|
323
|
+
// Wait for the async callback to execute
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
326
325
|
assert.called(services.initServiceCatalogs);
|
|
327
|
-
|
|
326
|
+
});
|
|
327
|
+
|
|
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);
|
|
328
339
|
});
|
|
329
340
|
|
|
330
341
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -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', () => {
|
|
@@ -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', () => {
|