@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.2
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/config.js +7 -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/services/service-url.js +11 -1
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +583 -94
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +507 -41
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/lib/services-v2/types.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 +7 -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/services/service-url.js +9 -1
- package/src/lib/services/services.js +433 -6
- package/src/lib/services-v2/services-v2.ts +417 -2
- package/src/lib/services-v2/types.ts +5 -0
- package/test/integration/spec/services/service-catalog.js +16 -11
- package/test/integration/spec/services/services.js +58 -9
- package/test/integration/spec/services-v2/services-v2.js +49 -6
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-url.js +110 -0
- package/test/unit/spec/services/services.js +692 -12
- package/test/unit/spec/services-v2/services-v2.ts +539 -0
- package/test/unit/spec/webex-core.js +2 -0
- package/test/unit/spec/webex-internal-core.js +2 -0
|
@@ -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();
|
|
@@ -112,6 +115,7 @@ describe('webex-core', () => {
|
|
|
112
115
|
services.initialize();
|
|
113
116
|
|
|
114
117
|
// call the onReady callback
|
|
118
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
115
119
|
services.listenToOnce.getCall(1).args[2]();
|
|
116
120
|
|
|
117
121
|
await waitForAsync();
|
|
@@ -122,6 +126,302 @@ describe('webex-core', () => {
|
|
|
122
126
|
`services: failed to init initial services when credentials available, ${expectedMessage}`
|
|
123
127
|
);
|
|
124
128
|
});
|
|
129
|
+
|
|
130
|
+
it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
|
|
131
|
+
services.listenToOnce = sinon.stub();
|
|
132
|
+
services.initialize();
|
|
133
|
+
|
|
134
|
+
// Fire the change:config handler which decides gated-vs-ungated and, in
|
|
135
|
+
// ungated mode, flips services.ready to true.
|
|
136
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
137
|
+
|
|
138
|
+
assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
|
|
142
|
+
services.listenToOnce = sinon.stub();
|
|
143
|
+
services.initialize();
|
|
144
|
+
|
|
145
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
146
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
147
|
+
|
|
148
|
+
// Call 0 is 'change:config'; call 1 is the catalog init listener.
|
|
149
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
150
|
+
assert.equal(event, 'ready', 'default path listens on webex ready');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('#initialize (waitForCatalogInit=true)', () => {
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
services.webex.config = {
|
|
157
|
+
...(services.webex.config || {}),
|
|
158
|
+
services: {waitForCatalogInit: true},
|
|
159
|
+
};
|
|
160
|
+
// Reset ready flag: the outer beforeEach constructs services which
|
|
161
|
+
// runs initialize() once with no config (ungated path, flips ready to
|
|
162
|
+
// true). We reset here so tests that call initialize() a second time
|
|
163
|
+
// with the flag enabled observe the gated behavior from a clean state.
|
|
164
|
+
services.ready = false;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
168
|
+
services.listenToOnce = sinon.stub();
|
|
169
|
+
services.initialize();
|
|
170
|
+
|
|
171
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
175
|
+
services.listenToOnce = sinon.stub();
|
|
176
|
+
services.initialize();
|
|
177
|
+
|
|
178
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
179
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
180
|
+
|
|
181
|
+
// Call 0 is 'change:config'; call 1 is the catalog init listener.
|
|
182
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
183
|
+
assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('flips services.ready=true after init succeeds with credentials', async () => {
|
|
187
|
+
services.listenToOnce = sinon.stub();
|
|
188
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
189
|
+
services.webex.credentials = {
|
|
190
|
+
supertoken: {access_token: 'token'},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
services.initialize();
|
|
194
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
195
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
196
|
+
await waitForAsync();
|
|
197
|
+
|
|
198
|
+
assert.isTrue(services.ready);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
|
|
202
|
+
services.listenToOnce = sinon.stub();
|
|
203
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
204
|
+
|
|
205
|
+
services.initialize();
|
|
206
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
207
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
208
|
+
await waitForAsync();
|
|
209
|
+
|
|
210
|
+
assert.isTrue(services.ready);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
|
|
214
|
+
services.listenToOnce = sinon.stub();
|
|
215
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
216
|
+
services.webex.credentials = {
|
|
217
|
+
supertoken: {access_token: 'token'},
|
|
218
|
+
};
|
|
219
|
+
services.logger.error = sinon.stub();
|
|
220
|
+
|
|
221
|
+
services.initialize();
|
|
222
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
223
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
224
|
+
await waitForAsync();
|
|
225
|
+
|
|
226
|
+
assert.isTrue(services.initFailed);
|
|
227
|
+
assert.isTrue(services.ready);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
|
|
231
|
+
services.listenToOnce = sinon.stub();
|
|
232
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
233
|
+
services.logger.error = sinon.stub();
|
|
234
|
+
|
|
235
|
+
services.initialize();
|
|
236
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
237
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
238
|
+
await waitForAsync();
|
|
239
|
+
|
|
240
|
+
assert.isTrue(services.initFailed);
|
|
241
|
+
assert.isTrue(services.ready);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('flips services.ready=true when init times out', async () => {
|
|
245
|
+
const clock = sinon.useFakeTimers();
|
|
246
|
+
services.listenToOnce = sinon.stub();
|
|
247
|
+
// Never resolves - the timeout must win the race.
|
|
248
|
+
services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
|
|
249
|
+
services.webex.credentials = {
|
|
250
|
+
supertoken: {access_token: 'token'},
|
|
251
|
+
};
|
|
252
|
+
services.logger.error = sinon.stub();
|
|
253
|
+
|
|
254
|
+
services.initialize();
|
|
255
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
256
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
257
|
+
|
|
258
|
+
// Advance past the 15s init timeout and flush microtasks so the
|
|
259
|
+
// rejected timeout can propagate through .catch and .finally into
|
|
260
|
+
// _finalizeReady().
|
|
261
|
+
await clock.tickAsync(15_001);
|
|
262
|
+
clock.restore();
|
|
263
|
+
|
|
264
|
+
assert.isTrue(services.initFailed);
|
|
265
|
+
assert.isTrue(services.ready);
|
|
266
|
+
sinon.assert.calledWithMatch(
|
|
267
|
+
services.logger.error,
|
|
268
|
+
/services: init timed out after 15000ms/
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
|
|
273
|
+
services.listenToOnce = sinon.stub();
|
|
274
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
275
|
+
|
|
276
|
+
let onChangeIsRefreshing;
|
|
277
|
+
services.webex.credentials = {
|
|
278
|
+
supertoken: {access_token: 'token'},
|
|
279
|
+
isRefreshing: true,
|
|
280
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
281
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
services.initialize();
|
|
286
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
287
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
288
|
+
await waitForAsync();
|
|
289
|
+
|
|
290
|
+
assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
|
|
291
|
+
assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
|
|
292
|
+
sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
|
|
293
|
+
|
|
294
|
+
// Simulate refresh completing.
|
|
295
|
+
onChangeIsRefreshing();
|
|
296
|
+
await waitForAsync();
|
|
297
|
+
|
|
298
|
+
assert.isTrue(services.ready);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
|
|
302
|
+
services.listenToOnce = sinon.stub();
|
|
303
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
304
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
305
|
+
|
|
306
|
+
services.initialize();
|
|
307
|
+
// initialize() creates a new catalog and replaces the WeakMap entry.
|
|
308
|
+
const currentCatalog = services._getCatalog();
|
|
309
|
+
// Fire the 'loaded' listener (index 1; index 0 was 'change:config').
|
|
310
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
311
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
312
|
+
await waitForAsync();
|
|
313
|
+
|
|
314
|
+
const freshLoginCall = services.listenToOnce
|
|
315
|
+
.getCalls()
|
|
316
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
317
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
318
|
+
|
|
319
|
+
// Simulate OAuth completing.
|
|
320
|
+
services.webex.canAuthorize = true;
|
|
321
|
+
currentCatalog.status.postauth.ready = false;
|
|
322
|
+
freshLoginCall.args[2]();
|
|
323
|
+
|
|
324
|
+
assert.isTrue(
|
|
325
|
+
services.initServiceCatalogs.called,
|
|
326
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
|
|
331
|
+
services.listenToOnce = sinon.stub();
|
|
332
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
333
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
334
|
+
|
|
335
|
+
services.initialize();
|
|
336
|
+
// initialize() creates a new catalog; grab the current one.
|
|
337
|
+
const currentCatalog = services._getCatalog();
|
|
338
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
339
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
340
|
+
await waitForAsync();
|
|
341
|
+
|
|
342
|
+
const freshLoginCall = services.listenToOnce
|
|
343
|
+
.getCalls()
|
|
344
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
345
|
+
|
|
346
|
+
services.webex.canAuthorize = true;
|
|
347
|
+
currentCatalog.status.postauth.ready = true; // already collected
|
|
348
|
+
freshLoginCall.args[2]();
|
|
349
|
+
|
|
350
|
+
assert.isFalse(
|
|
351
|
+
services.initServiceCatalogs.called,
|
|
352
|
+
'must not re-fetch postauth catalog if it is already ready'
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('finalizes ready when cache is warm (skips network fetch)', async () => {
|
|
357
|
+
services.listenToOnce = sinon.stub();
|
|
358
|
+
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
|
|
359
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
360
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
361
|
+
|
|
362
|
+
services.initialize();
|
|
363
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
364
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
365
|
+
await waitForAsync();
|
|
366
|
+
await waitForAsync();
|
|
367
|
+
|
|
368
|
+
assert.isTrue(services.ready);
|
|
369
|
+
assert.isFalse(
|
|
370
|
+
services.initServiceCatalogs.called,
|
|
371
|
+
'must skip network fetch when cache is warm'
|
|
372
|
+
);
|
|
373
|
+
assert.isFalse(
|
|
374
|
+
services.collectPreauthCatalog.called,
|
|
375
|
+
'must skip preauth fetch when cache is warm'
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('#_finalizeReady', () => {
|
|
381
|
+
beforeEach(() => {
|
|
382
|
+
// Outer beforeEach construction already ran initialize() (ungated,
|
|
383
|
+
// sets ready=true). Reset so _finalizeReady's flip is observable.
|
|
384
|
+
services.ready = false;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('sets ready=true immediately when credentials are not refreshing', async () => {
|
|
388
|
+
services.webex.credentials = {isRefreshing: false};
|
|
389
|
+
assert.isFalse(services.ready);
|
|
390
|
+
|
|
391
|
+
await services._finalizeReady();
|
|
392
|
+
|
|
393
|
+
assert.isTrue(services.ready);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('sets ready=true when credentials are missing', async () => {
|
|
397
|
+
services.webex.credentials = undefined;
|
|
398
|
+
|
|
399
|
+
await services._finalizeReady();
|
|
400
|
+
|
|
401
|
+
assert.isTrue(services.ready);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('awaits change:isRefreshing before setting ready=true', async () => {
|
|
405
|
+
let onChangeIsRefreshing;
|
|
406
|
+
services.webex.credentials = {
|
|
407
|
+
isRefreshing: true,
|
|
408
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
409
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
410
|
+
}),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const settled = services._finalizeReady();
|
|
414
|
+
// Let the async body reach its await point.
|
|
415
|
+
await waitForAsync();
|
|
416
|
+
|
|
417
|
+
assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
|
|
418
|
+
assert.isFunction(onChangeIsRefreshing);
|
|
419
|
+
|
|
420
|
+
onChangeIsRefreshing();
|
|
421
|
+
await settled;
|
|
422
|
+
|
|
423
|
+
assert.isTrue(services.ready);
|
|
424
|
+
});
|
|
125
425
|
});
|
|
126
426
|
|
|
127
427
|
describe('#initServiceCatalogs', () => {
|
|
@@ -349,8 +649,6 @@ describe('webex-core', () => {
|
|
|
349
649
|
|
|
350
650
|
const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
|
|
351
651
|
|
|
352
|
-
assert.deepEqual(mapResult, mapResponse);
|
|
353
|
-
|
|
354
652
|
assert.calledOnceWithExactly(services.request, {
|
|
355
653
|
method: 'GET',
|
|
356
654
|
service: 'u2c',
|
|
@@ -844,14 +1142,17 @@ describe('webex-core', () => {
|
|
|
844
1142
|
it('returns unique mobius host entries from hostCatalog', () => {
|
|
845
1143
|
// Arrange: two hostCatalog keys, with duplicate mobius host across keys
|
|
846
1144
|
services._hostCatalog = {
|
|
847
|
-
'mobius-
|
|
848
|
-
{host: 'mobius-
|
|
849
|
-
{host: 'mobius-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
'
|
|
853
|
-
{host: 'mobius-
|
|
854
|
-
|
|
1145
|
+
'mobius-us-east-2.prod.infra.webex.com': [
|
|
1146
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
|
|
1147
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
|
|
1148
|
+
],
|
|
1149
|
+
|
|
1150
|
+
'mobius-eu-central-1.prod.infra.webex.com': [
|
|
1151
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
|
|
1152
|
+
],
|
|
1153
|
+
'wdm-a.webex.com' : [
|
|
1154
|
+
{host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
|
|
1155
|
+
]
|
|
855
1156
|
};
|
|
856
1157
|
|
|
857
1158
|
// Act
|
|
@@ -862,12 +1163,13 @@ describe('webex-core', () => {
|
|
|
862
1163
|
assert.deepEqual(
|
|
863
1164
|
clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
|
|
864
1165
|
[
|
|
865
|
-
{host: 'mobius-
|
|
866
|
-
{host: 'mobius-
|
|
1166
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
|
|
1167
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
|
|
867
1168
|
]
|
|
868
1169
|
);
|
|
869
1170
|
});
|
|
870
1171
|
});
|
|
1172
|
+
|
|
871
1173
|
describe('#isValidHost', () => {
|
|
872
1174
|
beforeEach(() => {
|
|
873
1175
|
// Setting up a mock host catalog
|
|
@@ -919,6 +1221,384 @@ describe('webex-core', () => {
|
|
|
919
1221
|
assert.isFalse(services.isValidHost([]));
|
|
920
1222
|
});
|
|
921
1223
|
});
|
|
1224
|
+
|
|
1225
|
+
describe('#isIntegrationEnvironment', () => {
|
|
1226
|
+
it('returns true when u2c URL contains "intb"', () => {
|
|
1227
|
+
services.webex.config = {
|
|
1228
|
+
services: {
|
|
1229
|
+
discovery: {
|
|
1230
|
+
u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
assert.isTrue(services.isIntegrationEnvironment());
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('returns false when u2c URL does not contain "intb" (production)', () => {
|
|
1238
|
+
services.webex.config = {
|
|
1239
|
+
services: {
|
|
1240
|
+
discovery: {
|
|
1241
|
+
u2c: 'https://u2c.wbx2.com/u2c/api/v1',
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('returns false when u2c URL is for FedRAMP', () => {
|
|
1249
|
+
services.webex.config = {
|
|
1250
|
+
services: {
|
|
1251
|
+
discovery: {
|
|
1252
|
+
u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
};
|
|
1256
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
it('returns false when u2c URL is undefined', () => {
|
|
1260
|
+
services.webex.config = {
|
|
1261
|
+
services: {
|
|
1262
|
+
discovery: {
|
|
1263
|
+
u2c: undefined,
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
};
|
|
1267
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('returns false when config is not available', () => {
|
|
1271
|
+
services.webex.config = undefined;
|
|
1272
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
describe('U2C catalog cache behavior', () => {
|
|
1277
|
+
let webex;
|
|
1278
|
+
let services;
|
|
1279
|
+
let catalog;
|
|
1280
|
+
let localStorageBackup;
|
|
1281
|
+
let windowBackup;
|
|
1282
|
+
|
|
1283
|
+
const makeLocalStorageShim = () => {
|
|
1284
|
+
const store = new Map();
|
|
1285
|
+
return {
|
|
1286
|
+
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
1287
|
+
setItem: (k, v) => store.set(k, v),
|
|
1288
|
+
removeItem: (k) => store.delete(k),
|
|
1289
|
+
_store: store,
|
|
1290
|
+
};
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
beforeEach(() => {
|
|
1294
|
+
// Build a fresh webex instance
|
|
1295
|
+
webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
|
|
1296
|
+
services = webex.internal.services;
|
|
1297
|
+
catalog = services._getCatalog();
|
|
1298
|
+
|
|
1299
|
+
// enable U2C caching feature flag in tests that rely on localStorage writes/reads
|
|
1300
|
+
services.webex.config = services.webex.config || {};
|
|
1301
|
+
services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
|
|
1302
|
+
|
|
1303
|
+
// stub window.localStorage
|
|
1304
|
+
windowBackup = global.window;
|
|
1305
|
+
if (!global.window) global.window = {};
|
|
1306
|
+
localStorageBackup = global.window.localStorage;
|
|
1307
|
+
global.window.localStorage = makeLocalStorageShim();
|
|
1308
|
+
// Ensure code under test uses our shim via util method
|
|
1309
|
+
sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
|
|
1310
|
+
|
|
1311
|
+
// Stub the formatter so we don't need a full hostmap payload in tests
|
|
1312
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1313
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1314
|
+
]);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
afterEach(() => {
|
|
1318
|
+
global.window.localStorage = localStorageBackup || undefined;
|
|
1319
|
+
if (!windowBackup) {
|
|
1320
|
+
delete global.window;
|
|
1321
|
+
} else {
|
|
1322
|
+
global.window = windowBackup;
|
|
1323
|
+
}
|
|
1324
|
+
// Restore util stub if present
|
|
1325
|
+
if (services._getLocalStorageSafe && services._getLocalStorageSafe.restore) {
|
|
1326
|
+
services._getLocalStorageSafe.restore();
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
|
|
1331
|
+
// Arrange: authenticated credentials and spies
|
|
1332
|
+
services.webex.credentials = {
|
|
1333
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1334
|
+
canAuthorize: true,
|
|
1335
|
+
supertoken: {access_token: 'token'},
|
|
1336
|
+
};
|
|
1337
|
+
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
1338
|
+
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1339
|
+
const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
|
|
1340
|
+
// Make fetch return a hostmap object and allow formatter to reduce it
|
|
1341
|
+
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
1342
|
+
// Cause ready callback to run immediately
|
|
1343
|
+
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1344
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// Act
|
|
1348
|
+
services.initialize();
|
|
1349
|
+
await waitForAsync();
|
|
1350
|
+
|
|
1351
|
+
// Assert: initServiceCatalogs was called because there was no cache
|
|
1352
|
+
assert.isTrue(initSpy.called, 'expected initServiceCatalogs to be invoked on ready');
|
|
1353
|
+
// _cacheCatalog is called at least once (preauth/postauth flows)
|
|
1354
|
+
assert.isTrue(cacheSpy.called, 'expected _cacheCatalog to be called');
|
|
1355
|
+
assert.isTrue(setItemSpy.called, 'expected localStorage.setItem to be called');
|
|
1356
|
+
|
|
1357
|
+
// Cleanup spies
|
|
1358
|
+
services.request.restore();
|
|
1359
|
+
initSpy.restore();
|
|
1360
|
+
cacheSpy.restore();
|
|
1361
|
+
setItemSpy.restore();
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it('does not invoke initServiceCatalogs on ready when cache exists and uses cached catalog', async () => {
|
|
1365
|
+
// Arrange: put a valid cache
|
|
1366
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1367
|
+
const cached = {
|
|
1368
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1369
|
+
cachedAt: Date.now(),
|
|
1370
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1371
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1372
|
+
};
|
|
1373
|
+
global.window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(cached));
|
|
1374
|
+
|
|
1375
|
+
// authenticated credentials
|
|
1376
|
+
services.webex.credentials = {
|
|
1377
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1378
|
+
canAuthorize: true,
|
|
1379
|
+
supertoken: {access_token: 'token'},
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
1383
|
+
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1384
|
+
// Cause ready callback to run immediately
|
|
1385
|
+
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1386
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// Act
|
|
1390
|
+
services.initialize();
|
|
1391
|
+
await waitForAsync();
|
|
1392
|
+
|
|
1393
|
+
// Assert: ready path found cache and skipped initServiceCatalogs
|
|
1394
|
+
assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
|
|
1395
|
+
assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
|
|
1396
|
+
assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
|
|
1397
|
+
assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
|
|
1398
|
+
|
|
1399
|
+
// Cleanup
|
|
1400
|
+
initSpy.restore();
|
|
1401
|
+
cacheSpy.restore();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
it('expires cached catalog after TTL and clears the entry', async () => {
|
|
1405
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1406
|
+
const staleCached = {
|
|
1407
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1408
|
+
cachedAt: Date.now() - (24 * 60 * 60 * 1000 + 1000), // past TTL
|
|
1409
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1410
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
|
|
1414
|
+
|
|
1415
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1416
|
+
|
|
1417
|
+
assert.isFalse(warmed, 'stale cache must not warm');
|
|
1418
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
|
|
1419
|
+
assert.isFalse(catalog.status.preauth.ready);
|
|
1420
|
+
assert.isFalse(catalog.status.postauth.ready);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it('clearCatalogCache() removes the cached entry', async () => {
|
|
1424
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1425
|
+
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
|
|
1426
|
+
|
|
1427
|
+
await services.clearCatalogCache();
|
|
1428
|
+
|
|
1429
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
it('still fetches when forceRefresh=true even if ready', async () => {
|
|
1433
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1434
|
+
window.localStorage.setItem(
|
|
1435
|
+
CATALOG_CACHE_KEY_V1,
|
|
1436
|
+
JSON.stringify({
|
|
1437
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1438
|
+
cachedAt: Date.now(),
|
|
1439
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1440
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1441
|
+
})
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
// warm from cache
|
|
1445
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1446
|
+
assert.isTrue(warmed);
|
|
1447
|
+
assert.isTrue(catalog.status.preauth.ready);
|
|
1448
|
+
assert.isTrue(catalog.status.postauth.ready);
|
|
1449
|
+
|
|
1450
|
+
const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
|
|
1451
|
+
|
|
1452
|
+
// with forceRefresh we should fetch despite ready=true
|
|
1453
|
+
await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
|
|
1454
|
+
// pass an empty query to avoid spreading undefined in qs construction
|
|
1455
|
+
await services.updateServices({forceRefresh: true});
|
|
1456
|
+
|
|
1457
|
+
assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
|
|
1458
|
+
fetchSpy.restore();
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
it('stores selection metadata and env on cache write for preauth', async () => {
|
|
1462
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1463
|
+
// arrange config for env fingerprint
|
|
1464
|
+
services.webex.config = services.webex.config || {};
|
|
1465
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1466
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1467
|
+
services.webex.config.fedramp = false;
|
|
1468
|
+
|
|
1469
|
+
// write cache with meta
|
|
1470
|
+
await services._cacheCatalog(
|
|
1471
|
+
'preauth',
|
|
1472
|
+
{serviceLinks: {}, hostCatalog: {}},
|
|
1473
|
+
{selectionType: 'orgId', selectionValue: 'urn:EXAMPLE:org'}
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
const raw = window.localStorage.getItem(CATALOG_CACHE_KEY_V1);
|
|
1477
|
+
assert.isString(raw);
|
|
1478
|
+
const parsed = JSON.parse(raw);
|
|
1479
|
+
assert.equal(parsed.orgId, undefined, 'orgId not set without credentials');
|
|
1480
|
+
assert.deepEqual(parsed.env, {
|
|
1481
|
+
fedramp: false,
|
|
1482
|
+
u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1',
|
|
1483
|
+
});
|
|
1484
|
+
assert.isObject(parsed.preauth);
|
|
1485
|
+
assert.deepEqual(parsed.preauth.meta, {
|
|
1486
|
+
selectionType: 'orgId',
|
|
1487
|
+
selectionValue: 'urn:EXAMPLE:org',
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
it('warms preauth from cache when selection meta matches intended orgId', async () => {
|
|
1492
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1493
|
+
// stub credentials
|
|
1494
|
+
services.webex.credentials = {
|
|
1495
|
+
canAuthorize: true,
|
|
1496
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1497
|
+
};
|
|
1498
|
+
// set current env to match cached env
|
|
1499
|
+
services.webex.config = services.webex.config || {};
|
|
1500
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1501
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1502
|
+
services.webex.config.fedramp = false;
|
|
1503
|
+
// cache with matching orgId selection
|
|
1504
|
+
window.localStorage.setItem(
|
|
1505
|
+
CATALOG_CACHE_KEY_V1,
|
|
1506
|
+
JSON.stringify({
|
|
1507
|
+
cachedAt: Date.now(),
|
|
1508
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1509
|
+
preauth: {
|
|
1510
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1511
|
+
meta: {selectionType: 'orgId', selectionValue: 'urn:EXAMPLE:org'},
|
|
1512
|
+
},
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1515
|
+
// formatter returns at least one entry to mark ready
|
|
1516
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1517
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1518
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1519
|
+
]);
|
|
1520
|
+
|
|
1521
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1522
|
+
assert.isTrue(warmed);
|
|
1523
|
+
assert.isTrue(catalog.status.preauth.ready, 'preauth should be warmed on match');
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it('does not warm preauth when selection meta is proximity mode', async () => {
|
|
1527
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1528
|
+
// cache with proximity mode selection
|
|
1529
|
+
window.localStorage.setItem(
|
|
1530
|
+
CATALOG_CACHE_KEY_V1,
|
|
1531
|
+
JSON.stringify({
|
|
1532
|
+
cachedAt: Date.now(),
|
|
1533
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1534
|
+
preauth: {
|
|
1535
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1536
|
+
meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
|
|
1537
|
+
},
|
|
1538
|
+
})
|
|
1539
|
+
);
|
|
1540
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1541
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1542
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1543
|
+
]);
|
|
1544
|
+
|
|
1545
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1546
|
+
// function returns true if overall cache path succeeded; we only verify group readiness
|
|
1547
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm for proximity mode');
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('does not warm preauth when selection meta mismatches intended selection', async () => {
|
|
1551
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1552
|
+
// authorized with org X
|
|
1553
|
+
services.webex.credentials = {
|
|
1554
|
+
canAuthorize: true,
|
|
1555
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1556
|
+
};
|
|
1557
|
+
// cache points to a different org
|
|
1558
|
+
window.localStorage.setItem(
|
|
1559
|
+
CATALOG_CACHE_KEY_V1,
|
|
1560
|
+
JSON.stringify({
|
|
1561
|
+
cachedAt: Date.now(),
|
|
1562
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1563
|
+
preauth: {
|
|
1564
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1565
|
+
meta: {selectionType: 'orgId', selectionValue: 'urn:DIFF:org'},
|
|
1566
|
+
},
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1570
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1571
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1572
|
+
]);
|
|
1573
|
+
|
|
1574
|
+
await services._loadCatalogFromCache();
|
|
1575
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
it('skips warming when environment fingerprint mismatches', async () => {
|
|
1579
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1580
|
+
// cached env differs from current env (different U2C URL)
|
|
1581
|
+
window.localStorage.setItem(
|
|
1582
|
+
CATALOG_CACHE_KEY_V1,
|
|
1583
|
+
JSON.stringify({
|
|
1584
|
+
cachedAt: Date.now(),
|
|
1585
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
|
|
1586
|
+
preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
|
|
1587
|
+
})
|
|
1588
|
+
);
|
|
1589
|
+
// current env
|
|
1590
|
+
services.webex.config = services.webex.config || {};
|
|
1591
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1592
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1593
|
+
services.webex.config.fedramp = false;
|
|
1594
|
+
|
|
1595
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1596
|
+
assert.isFalse(warmed, 'env mismatch should skip warm and return false');
|
|
1597
|
+
assert.isFalse(catalog.status.preauth.ready);
|
|
1598
|
+
assert.isFalse(catalog.status.postauth.ready);
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
|
|
922
1602
|
});
|
|
923
1603
|
});
|
|
924
1604
|
/* eslint-enable no-underscore-dangle */
|