@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.
- package/README.md +5 -2
- package/dist/config.js +38 -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/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/catalog-url.js +87 -0
- package/dist/interceptors/catalog-url.js.map +1 -0
- package/dist/interceptors/payload-transformer.js +25 -4
- package/dist/interceptors/payload-transformer.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 +103 -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 +108 -18
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +232 -34
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +14 -13
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +241 -37
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +11 -3
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +42 -0
- package/src/credentials-config.js +13 -0
- package/src/index.js +1 -0
- package/src/interceptors/catalog-url.js +66 -0
- package/src/interceptors/payload-transformer.js +23 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +93 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +112 -16
- package/src/lib/services/services.js +208 -20
- package/src/lib/services-v2/service-catalog.ts +16 -12
- package/src/lib/services-v2/services-v2.ts +218 -21
- package/src/webex-core.js +8 -0
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/services.js +183 -112
- package/test/integration/spec/services-v2/services-v2.js +169 -98
- package/test/unit/spec/credentials/credentials.js +247 -3
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/interceptors/catalog-url.js +224 -0
- package/test/unit/spec/interceptors/payload-transformer.js +119 -1
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +368 -11
- package/test/unit/spec/services/services.js +440 -2
- package/test/unit/spec/services-v2/service-catalog.ts +134 -11
- package/test/unit/spec/services-v2/services-v2.ts +467 -0
- package/test/unit/spec/webex-core.js +77 -1
|
@@ -46,6 +46,7 @@ describe('webex-core', () => {
|
|
|
46
46
|
services.initialize();
|
|
47
47
|
|
|
48
48
|
// call the onReady callback
|
|
49
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
49
50
|
services.listenToOnce.getCall(1).args[2]();
|
|
50
51
|
await waitForAsync();
|
|
51
52
|
|
|
@@ -59,6 +60,7 @@ describe('webex-core', () => {
|
|
|
59
60
|
services.initialize();
|
|
60
61
|
|
|
61
62
|
// call the onReady callback
|
|
63
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
62
64
|
services.listenToOnce.getCall(1).args[2]();
|
|
63
65
|
await waitForAsync();
|
|
64
66
|
|
|
@@ -81,6 +83,7 @@ describe('webex-core', () => {
|
|
|
81
83
|
services.initialize();
|
|
82
84
|
|
|
83
85
|
// call the onReady callback
|
|
86
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
84
87
|
services.listenToOnce.getCall(1).args[2]();
|
|
85
88
|
|
|
86
89
|
await waitForAsync();
|
|
@@ -114,6 +117,7 @@ describe('webex-core', () => {
|
|
|
114
117
|
services.initialize();
|
|
115
118
|
|
|
116
119
|
// call the onReady callback
|
|
120
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
117
121
|
services.listenToOnce.getCall(1).args[2]();
|
|
118
122
|
|
|
119
123
|
await waitForAsync();
|
|
@@ -125,6 +129,433 @@ describe('webex-core', () => {
|
|
|
125
129
|
);
|
|
126
130
|
}
|
|
127
131
|
);
|
|
132
|
+
|
|
133
|
+
it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
|
|
134
|
+
services.listenToOnce = sinon.stub();
|
|
135
|
+
services.initialize();
|
|
136
|
+
|
|
137
|
+
// Fire the change:config handler which decides gated-vs-ungated and, in
|
|
138
|
+
// ungated mode, flips services.ready to true.
|
|
139
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
140
|
+
|
|
141
|
+
assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
|
|
145
|
+
services.listenToOnce = sinon.stub();
|
|
146
|
+
services.initialize();
|
|
147
|
+
|
|
148
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
149
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
150
|
+
|
|
151
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
152
|
+
assert.equal(event, 'ready', 'default path listens on webex ready');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('skips preauth catalog collection when skipPreauthCatalogOnUnauthenticated is true and no credentials', async () => {
|
|
156
|
+
services.webex.config = {
|
|
157
|
+
...(services.webex.config || {}),
|
|
158
|
+
services: {skipPreauthCatalogOnUnauthenticated: true},
|
|
159
|
+
};
|
|
160
|
+
services.listenToOnce = sinon.stub();
|
|
161
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
162
|
+
services.logger.info = sinon.stub();
|
|
163
|
+
|
|
164
|
+
services.initialize();
|
|
165
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
166
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
167
|
+
await waitForAsync();
|
|
168
|
+
|
|
169
|
+
assert.isFalse(
|
|
170
|
+
services.collectPreauthCatalog.called,
|
|
171
|
+
'must not collect preauth catalog while unauthenticated when option is enabled'
|
|
172
|
+
);
|
|
173
|
+
assert.isFalse(services.initFailed);
|
|
174
|
+
sinon.assert.calledWith(
|
|
175
|
+
services.logger.info,
|
|
176
|
+
'services: skipping preauth catalog collection while unauthenticated as per the config'
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('still collects preauth catalog when skipPreauthCatalogOnUnauthenticated is false (default)', async () => {
|
|
181
|
+
services.webex.config = {
|
|
182
|
+
...(services.webex.config || {}),
|
|
183
|
+
services: {skipPreauthCatalogOnUnauthenticated: false},
|
|
184
|
+
};
|
|
185
|
+
services.listenToOnce = sinon.stub();
|
|
186
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
187
|
+
|
|
188
|
+
services.initialize();
|
|
189
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
190
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
191
|
+
await waitForAsync();
|
|
192
|
+
|
|
193
|
+
assert.isTrue(
|
|
194
|
+
services.collectPreauthCatalog.called,
|
|
195
|
+
'must collect preauth catalog when option is disabled'
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('#initialize (waitForCatalogInit=true)', () => {
|
|
201
|
+
let clock;
|
|
202
|
+
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
services.webex.config = {
|
|
205
|
+
...(services.webex.config || {}),
|
|
206
|
+
services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
|
|
207
|
+
};
|
|
208
|
+
// Reset ready flag: the outer beforeEach constructs services which
|
|
209
|
+
// runs initialize() once with no config (ungated path, flips ready to
|
|
210
|
+
// true). We reset here so tests that call initialize() a second time
|
|
211
|
+
// with the flag enabled observe the gated behavior from a clean state.
|
|
212
|
+
services.ready = false;
|
|
213
|
+
// The gated path schedules a real 15s setTimeout via _makeInitTimeout()
|
|
214
|
+
// that is not cleared when the init race resolves. Fake only setTimeout/
|
|
215
|
+
// clearTimeout (leaving setImmediate real so waitForAsync still works)
|
|
216
|
+
// so restoring in afterEach clears the pending timer and Jest can exit.
|
|
217
|
+
clock = sinon.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
clock.restore();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
225
|
+
services.listenToOnce = sinon.stub();
|
|
226
|
+
services.initialize();
|
|
227
|
+
|
|
228
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
232
|
+
services.listenToOnce = sinon.stub();
|
|
233
|
+
services.initialize();
|
|
234
|
+
|
|
235
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
236
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
237
|
+
|
|
238
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
239
|
+
assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('flips services.ready=true after init succeeds with credentials', async () => {
|
|
243
|
+
services.listenToOnce = sinon.stub();
|
|
244
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
245
|
+
services.webex.credentials = {
|
|
246
|
+
supertoken: {access_token: 'token'},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
services.initialize();
|
|
250
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
251
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
252
|
+
await waitForAsync();
|
|
253
|
+
|
|
254
|
+
assert.isTrue(services.ready);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
|
|
258
|
+
services.listenToOnce = sinon.stub();
|
|
259
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
260
|
+
|
|
261
|
+
services.initialize();
|
|
262
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
263
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
264
|
+
await waitForAsync();
|
|
265
|
+
|
|
266
|
+
assert.isTrue(services.ready);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
|
|
270
|
+
services.listenToOnce = sinon.stub();
|
|
271
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
272
|
+
services.webex.credentials = {
|
|
273
|
+
supertoken: {access_token: 'token'},
|
|
274
|
+
};
|
|
275
|
+
services.logger.error = sinon.stub();
|
|
276
|
+
|
|
277
|
+
services.initialize();
|
|
278
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
279
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
280
|
+
await waitForAsync();
|
|
281
|
+
|
|
282
|
+
assert.isTrue(services.initFailed);
|
|
283
|
+
assert.isTrue(services.ready);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
|
|
287
|
+
services.listenToOnce = sinon.stub();
|
|
288
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
289
|
+
services.logger.error = sinon.stub();
|
|
290
|
+
|
|
291
|
+
services.initialize();
|
|
292
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
293
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
294
|
+
await waitForAsync();
|
|
295
|
+
|
|
296
|
+
assert.isTrue(services.initFailed);
|
|
297
|
+
assert.isTrue(services.ready);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('flips services.ready=true when init times out', async () => {
|
|
301
|
+
services.listenToOnce = sinon.stub();
|
|
302
|
+
services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
|
|
303
|
+
services.webex.credentials = {
|
|
304
|
+
supertoken: {access_token: 'token'},
|
|
305
|
+
};
|
|
306
|
+
services.logger.error = sinon.stub();
|
|
307
|
+
|
|
308
|
+
services.initialize();
|
|
309
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
310
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
311
|
+
|
|
312
|
+
await clock.tickAsync(15_001);
|
|
313
|
+
|
|
314
|
+
assert.isTrue(services.initFailed);
|
|
315
|
+
assert.isTrue(services.ready);
|
|
316
|
+
sinon.assert.calledWithMatch(
|
|
317
|
+
services.logger.error,
|
|
318
|
+
/services: init timed out after 15000ms/
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
|
|
323
|
+
services.listenToOnce = sinon.stub();
|
|
324
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
325
|
+
|
|
326
|
+
let onChangeIsRefreshing: (() => void) | undefined;
|
|
327
|
+
services.webex.credentials = {
|
|
328
|
+
supertoken: {access_token: 'token'},
|
|
329
|
+
isRefreshing: true,
|
|
330
|
+
once: sinon.stub().callsFake((event: string, cb: () => void) => {
|
|
331
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
332
|
+
}),
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
services.initialize();
|
|
336
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
337
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
338
|
+
await waitForAsync();
|
|
339
|
+
|
|
340
|
+
assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
|
|
341
|
+
assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
|
|
342
|
+
sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
|
|
343
|
+
|
|
344
|
+
onChangeIsRefreshing!();
|
|
345
|
+
await waitForAsync();
|
|
346
|
+
|
|
347
|
+
assert.isTrue(services.ready);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
|
|
351
|
+
services.listenToOnce = sinon.stub();
|
|
352
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
353
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
354
|
+
|
|
355
|
+
services.initialize();
|
|
356
|
+
// initialize() creates a new catalog and replaces the WeakMap entry.
|
|
357
|
+
const currentCatalog = services._getCatalog();
|
|
358
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
359
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
360
|
+
await waitForAsync();
|
|
361
|
+
|
|
362
|
+
const freshLoginCall = services.listenToOnce
|
|
363
|
+
.getCalls()
|
|
364
|
+
.find((call: any) => call.args[1] === 'change:canAuthorize');
|
|
365
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
366
|
+
|
|
367
|
+
services.webex.canAuthorize = true;
|
|
368
|
+
currentCatalog.status.postauth.ready = false;
|
|
369
|
+
freshLoginCall!.args[2]();
|
|
370
|
+
|
|
371
|
+
assert.isTrue(
|
|
372
|
+
services.initServiceCatalogs.called,
|
|
373
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// `catalog.isReady` is now set inside initServiceCatalogs (after the
|
|
377
|
+
// postauth updateServices) rather than by this listener, so it is not
|
|
378
|
+
// asserted here where initServiceCatalogs is stubbed.
|
|
379
|
+
await waitForAsync();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
|
|
383
|
+
services.listenToOnce = sinon.stub();
|
|
384
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
385
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
386
|
+
|
|
387
|
+
services.initialize();
|
|
388
|
+
// initialize() creates a new catalog; grab the current one.
|
|
389
|
+
const currentCatalog = services._getCatalog();
|
|
390
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
391
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
392
|
+
await waitForAsync();
|
|
393
|
+
|
|
394
|
+
const freshLoginCall = services.listenToOnce
|
|
395
|
+
.getCalls()
|
|
396
|
+
.find((call: any) => call.args[1] === 'change:canAuthorize');
|
|
397
|
+
|
|
398
|
+
services.webex.canAuthorize = true;
|
|
399
|
+
currentCatalog.status.postauth.ready = true;
|
|
400
|
+
freshLoginCall!.args[2]();
|
|
401
|
+
|
|
402
|
+
assert.isFalse(
|
|
403
|
+
services.initServiceCatalogs.called,
|
|
404
|
+
'must not re-fetch postauth catalog if it is already ready'
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('finalizes ready when cache is warm (skips network fetch)', async () => {
|
|
409
|
+
services.listenToOnce = sinon.stub();
|
|
410
|
+
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
|
|
411
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
412
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
413
|
+
|
|
414
|
+
services.initialize();
|
|
415
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
416
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
417
|
+
await waitForAsync();
|
|
418
|
+
await waitForAsync();
|
|
419
|
+
|
|
420
|
+
assert.isTrue(services.ready);
|
|
421
|
+
assert.isFalse(
|
|
422
|
+
services.initServiceCatalogs.called,
|
|
423
|
+
'must skip network fetch when cache is warm'
|
|
424
|
+
);
|
|
425
|
+
assert.isFalse(
|
|
426
|
+
services.collectPreauthCatalog.called,
|
|
427
|
+
'must skip preauth fetch when cache is warm'
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe('#initialize (waitForCatalogInit=true, skipPreauthCatalogOnUnauthenticated=true)', () => {
|
|
433
|
+
beforeEach(() => {
|
|
434
|
+
services.webex.config = {
|
|
435
|
+
...(services.webex.config || {}),
|
|
436
|
+
services: {
|
|
437
|
+
waitForCatalogInit: true,
|
|
438
|
+
catalogInitTimeout: 15000,
|
|
439
|
+
skipPreauthCatalogOnUnauthenticated: true,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
services.ready = false;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('skips preauth catalog collection but still finalizes services.ready when unauthenticated', async () => {
|
|
446
|
+
services.listenToOnce = sinon.stub();
|
|
447
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
448
|
+
services.logger.info = sinon.stub();
|
|
449
|
+
|
|
450
|
+
services.initialize();
|
|
451
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
452
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
453
|
+
await waitForAsync();
|
|
454
|
+
|
|
455
|
+
assert.isFalse(
|
|
456
|
+
services.collectPreauthCatalog.called,
|
|
457
|
+
'must not collect preauth catalog while unauthenticated when option is enabled'
|
|
458
|
+
);
|
|
459
|
+
assert.isTrue(services.ready, 'must still finalize ready so webex.ready is not stalled');
|
|
460
|
+
sinon.assert.calledWith(
|
|
461
|
+
services.logger.info,
|
|
462
|
+
'services: skipping preauth catalog collection while unauthenticated as per the config'
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('still registers a change:canAuthorize listener when skipping preauth (fresh-login path)', async () => {
|
|
467
|
+
services.listenToOnce = sinon.stub();
|
|
468
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
469
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
470
|
+
|
|
471
|
+
services.initialize();
|
|
472
|
+
const currentCatalog = services._getCatalog();
|
|
473
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
474
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
475
|
+
await waitForAsync();
|
|
476
|
+
|
|
477
|
+
const freshLoginCall = services.listenToOnce
|
|
478
|
+
.getCalls()
|
|
479
|
+
.find((call: any) => call.args[1] === 'change:canAuthorize');
|
|
480
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
481
|
+
|
|
482
|
+
services.webex.canAuthorize = true;
|
|
483
|
+
currentCatalog.status.postauth.ready = false;
|
|
484
|
+
freshLoginCall!.args[2]();
|
|
485
|
+
|
|
486
|
+
assert.isTrue(
|
|
487
|
+
services.initServiceCatalogs.called,
|
|
488
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
489
|
+
);
|
|
490
|
+
await waitForAsync();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('does not schedule an init timeout when skipping preauth (no stray timer/rejection)', async () => {
|
|
494
|
+
const clock = sinon.useFakeTimers();
|
|
495
|
+
services.listenToOnce = sinon.stub();
|
|
496
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
497
|
+
services.logger.error = sinon.stub();
|
|
498
|
+
|
|
499
|
+
services.initialize();
|
|
500
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
501
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
502
|
+
|
|
503
|
+
// Advance well past the configured init timeout; since no timeout was
|
|
504
|
+
// scheduled in skip mode, no timeout error should ever be logged.
|
|
505
|
+
await clock.tickAsync(30_000);
|
|
506
|
+
clock.restore();
|
|
507
|
+
|
|
508
|
+
sinon.assert.neverCalledWithMatch(
|
|
509
|
+
services.logger.error,
|
|
510
|
+
/services: init timed out/
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
describe('#_finalizeReady', () => {
|
|
516
|
+
beforeEach(() => {
|
|
517
|
+
// Outer beforeEach construction already ran initialize() (ungated,
|
|
518
|
+
// sets ready=true). Reset so _finalizeReady's flip is observable.
|
|
519
|
+
services.ready = false;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('sets ready=true immediately when credentials are not refreshing', async () => {
|
|
523
|
+
services.webex.credentials = {isRefreshing: false};
|
|
524
|
+
assert.isFalse(services.ready);
|
|
525
|
+
|
|
526
|
+
await services._finalizeReady();
|
|
527
|
+
|
|
528
|
+
assert.isTrue(services.ready);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('sets ready=true when credentials are missing', async () => {
|
|
532
|
+
services.webex.credentials = undefined;
|
|
533
|
+
|
|
534
|
+
await services._finalizeReady();
|
|
535
|
+
|
|
536
|
+
assert.isTrue(services.ready);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('awaits change:isRefreshing before setting ready=true', async () => {
|
|
540
|
+
let onChangeIsRefreshing: (() => void) | undefined;
|
|
541
|
+
services.webex.credentials = {
|
|
542
|
+
isRefreshing: true,
|
|
543
|
+
once: sinon.stub().callsFake((event: string, cb: () => void) => {
|
|
544
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const settled = services._finalizeReady();
|
|
549
|
+
await waitForAsync();
|
|
550
|
+
|
|
551
|
+
assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
|
|
552
|
+
assert.isFunction(onChangeIsRefreshing);
|
|
553
|
+
|
|
554
|
+
onChangeIsRefreshing!();
|
|
555
|
+
await settled;
|
|
556
|
+
|
|
557
|
+
assert.isTrue(services.ready);
|
|
558
|
+
});
|
|
128
559
|
});
|
|
129
560
|
|
|
130
561
|
describe('#initServiceCatalogs', () => {
|
|
@@ -380,6 +811,22 @@ describe('webex-core', () => {
|
|
|
380
811
|
);
|
|
381
812
|
});
|
|
382
813
|
|
|
814
|
+
it('includes the catalog override query when configured', async () => {
|
|
815
|
+
services.webex.config.services = {useCatalogOverride: true};
|
|
816
|
+
sinon.stub(services, '_formatReceivedHostmap').resolves('map response');
|
|
817
|
+
sinon.stub(services, 'request').resolves({});
|
|
818
|
+
|
|
819
|
+
await services._fetchNewServiceHostmap({from: 'limited'});
|
|
820
|
+
|
|
821
|
+
assert.calledOnceWithExactly(services.request, {
|
|
822
|
+
method: 'GET',
|
|
823
|
+
service: 'u2c',
|
|
824
|
+
resource: '/limited/catalog',
|
|
825
|
+
qs: {format: 'U2CV2', useCatalogOverride: true},
|
|
826
|
+
headers: {},
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
383
830
|
it('checks service request rejects', async () => {
|
|
384
831
|
const error = new Error('some error');
|
|
385
832
|
|
|
@@ -449,6 +896,26 @@ describe('webex-core', () => {
|
|
|
449
896
|
});
|
|
450
897
|
});
|
|
451
898
|
|
|
899
|
+
describe('#getServiceFromUrl()', () => {
|
|
900
|
+
it('matches an exact service URL when the catalog URL has a trailing slash', () => {
|
|
901
|
+
const baseUrl = 'https://example.com/api/v1/';
|
|
902
|
+
|
|
903
|
+
catalog.updateServiceGroups('preauth', [
|
|
904
|
+
{
|
|
905
|
+
id: 'example',
|
|
906
|
+
serviceName: 'example',
|
|
907
|
+
serviceUrls: [{host: 'example.com', baseUrl, priority: 1}],
|
|
908
|
+
},
|
|
909
|
+
]);
|
|
910
|
+
|
|
911
|
+
assert.deepEqual(services.getServiceFromUrl('https://example.com/api/v1'), {
|
|
912
|
+
name: 'example',
|
|
913
|
+
priorityUrl: baseUrl,
|
|
914
|
+
defaultUrl: baseUrl,
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
452
919
|
describe('#_formatReceivedHostmap()', () => {
|
|
453
920
|
let serviceHostmap;
|
|
454
921
|
let formattedHM;
|
|
@@ -91,6 +91,30 @@ describe('Webex', () => {
|
|
|
91
91
|
assert.isFalse(webex.canAuthorize);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('initializes network telemetry after applying the configured metrics options', async () => {
|
|
95
|
+
const configuredWebex = new WebexCore({
|
|
96
|
+
config: {
|
|
97
|
+
metrics: {
|
|
98
|
+
networkTelemetry: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
intervalMs: 60 * 1_000,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const submitClientMetrics = sinon
|
|
106
|
+
.stub(configuredWebex.internal.metrics, 'submitClientMetrics')
|
|
107
|
+
.resolves();
|
|
108
|
+
|
|
109
|
+
assert.exists(configuredWebex.internal.metrics.networkTelemetry);
|
|
110
|
+
configuredWebex.trigger('request:start', {service: 'hydra', resource: 'rooms'});
|
|
111
|
+
await configuredWebex.internal.metrics.flushNetworkTelemetry();
|
|
112
|
+
|
|
113
|
+
assert.calledOnce(submitClientMetrics);
|
|
114
|
+
|
|
115
|
+
await configuredWebex.internal.metrics.stopNetworkTelemetry();
|
|
116
|
+
});
|
|
117
|
+
|
|
94
118
|
[
|
|
95
119
|
'data',
|
|
96
120
|
'data.access_token',
|
|
@@ -179,7 +203,7 @@ describe('Webex', () => {
|
|
|
179
203
|
|
|
180
204
|
describe('initializes with interceptors', () => {
|
|
181
205
|
[
|
|
182
|
-
// 4 pre, 4 post, 10 remaining default = 18
|
|
206
|
+
// 4 pre (CatalogUrlInterceptor is opt-in), 4 post, 10 remaining default = 18
|
|
183
207
|
[
|
|
184
208
|
'defaults to existing interceptors if undefined',
|
|
185
209
|
undefined,
|
|
@@ -205,6 +229,58 @@ describe('Webex', () => {
|
|
|
205
229
|
'RateLimitInterceptor',
|
|
206
230
|
],
|
|
207
231
|
],
|
|
232
|
+
[
|
|
233
|
+
'does not include CatalogUrlInterceptor for a truthy non-boolean value',
|
|
234
|
+
{services: {validateCatalogUrls: 'true'}},
|
|
235
|
+
18,
|
|
236
|
+
[
|
|
237
|
+
'RequestTimingInterceptor',
|
|
238
|
+
'RequestEventInterceptor',
|
|
239
|
+
'WebexTrackingIdInterceptor',
|
|
240
|
+
'RateLimitInterceptor',
|
|
241
|
+
'ServiceInterceptor',
|
|
242
|
+
'UserAgentInterceptor',
|
|
243
|
+
'ProxyInterceptor',
|
|
244
|
+
'WebexUserAgentInterceptor',
|
|
245
|
+
'AuthInterceptor',
|
|
246
|
+
'PayloadTransformerInterceptor',
|
|
247
|
+
'RedirectInterceptor',
|
|
248
|
+
'DefaultOptionsInterceptor',
|
|
249
|
+
'HostMapInterceptor',
|
|
250
|
+
'ServerErrorInterceptor',
|
|
251
|
+
'HttpStatusInterceptor',
|
|
252
|
+
'NetworkTimingInterceptor',
|
|
253
|
+
'EmbargoInterceptor',
|
|
254
|
+
'RateLimitInterceptor',
|
|
255
|
+
],
|
|
256
|
+
],
|
|
257
|
+
// CatalogUrlInterceptor is opt-in via services.validateCatalogUrls
|
|
258
|
+
[
|
|
259
|
+
'includes CatalogUrlInterceptor when validateCatalogUrls is enabled',
|
|
260
|
+
{services: {validateCatalogUrls: true}},
|
|
261
|
+
19,
|
|
262
|
+
[
|
|
263
|
+
'RequestTimingInterceptor',
|
|
264
|
+
'RequestEventInterceptor',
|
|
265
|
+
'WebexTrackingIdInterceptor',
|
|
266
|
+
'RateLimitInterceptor',
|
|
267
|
+
'CatalogUrlInterceptor',
|
|
268
|
+
'ServiceInterceptor',
|
|
269
|
+
'UserAgentInterceptor',
|
|
270
|
+
'ProxyInterceptor',
|
|
271
|
+
'WebexUserAgentInterceptor',
|
|
272
|
+
'AuthInterceptor',
|
|
273
|
+
'PayloadTransformerInterceptor',
|
|
274
|
+
'RedirectInterceptor',
|
|
275
|
+
'DefaultOptionsInterceptor',
|
|
276
|
+
'HostMapInterceptor',
|
|
277
|
+
'ServerErrorInterceptor',
|
|
278
|
+
'HttpStatusInterceptor',
|
|
279
|
+
'NetworkTimingInterceptor',
|
|
280
|
+
'EmbargoInterceptor',
|
|
281
|
+
'RateLimitInterceptor',
|
|
282
|
+
],
|
|
283
|
+
],
|
|
208
284
|
[
|
|
209
285
|
'only adds PayloadTransformerInterceptor',
|
|
210
286
|
{interceptors: {PayloadTransformerInterceptor: PayloadTransformerInterceptor.create}},
|