@webex/webex-core 3.12.0-webex-services-ready.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.
@@ -34,53 +34,6 @@ describe('webex-core', () => {
34
34
  });
35
35
 
36
36
  describe('#initialize', () => {
37
- it('listens for "loaded" event instead of "ready" to avoid deadlock', () => {
38
- services.listenToOnce = sinon.stub();
39
- services.initialize();
40
-
41
- // Second listenToOnce call should be for 'loaded' event
42
- assert.equal(services.listenToOnce.getCall(1).args[1], 'loaded');
43
- });
44
-
45
- it('services.ready starts as false', () => {
46
- assert.isFalse(services.ready);
47
- });
48
-
49
- it('sets ready to true and triggers services:initialized when initialization succeeds with credentials', async () => {
50
- services.listenToOnce = sinon.stub();
51
- services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
52
- services.trigger = sinon.stub();
53
- services.webex.credentials = {
54
- supertoken: {
55
- access_token: 'token',
56
- },
57
- };
58
-
59
- services.initialize();
60
-
61
- // call the onLoaded callback
62
- services.listenToOnce.getCall(1).args[2]();
63
- await waitForAsync();
64
-
65
- assert.isTrue(services.ready);
66
- sinon.assert.calledWith(services.trigger, 'services:initialized');
67
- });
68
-
69
- it('sets ready to true and triggers services:initialized when initialization succeeds without credentials', async () => {
70
- services.listenToOnce = sinon.stub();
71
- services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
72
- services.trigger = sinon.stub();
73
-
74
- services.initialize();
75
-
76
- // call the onLoaded callback
77
- services.listenToOnce.getCall(1).args[2]();
78
- await waitForAsync();
79
-
80
- assert.isTrue(services.ready);
81
- sinon.assert.calledWith(services.trigger, 'services:initialized');
82
- });
83
-
84
37
  it('initFailed is false when initialization succeeds and credentials are available', async () => {
85
38
  services.listenToOnce = sinon.stub();
86
39
  services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
@@ -92,7 +45,8 @@ describe('webex-core', () => {
92
45
 
93
46
  services.initialize();
94
47
 
95
- // call the onLoaded callback
48
+ // call the onReady callback
49
+ services.listenToOnce.getCall(0).args[2]();
96
50
  services.listenToOnce.getCall(1).args[2]();
97
51
  await waitForAsync();
98
52
 
@@ -105,7 +59,8 @@ describe('webex-core', () => {
105
59
 
106
60
  services.initialize();
107
61
 
108
- // call the onLoaded callback
62
+ // call the onReady callback
63
+ services.listenToOnce.getCall(0).args[2]();
109
64
  services.listenToOnce.getCall(1).args[2]();
110
65
  await waitForAsync();
111
66
 
@@ -114,9 +69,9 @@ describe('webex-core', () => {
114
69
 
115
70
  it.each([
116
71
  {error: new Error('failed'), expectedMessage: 'failed'},
117
- {error: undefined, expectedMessage: undefined},
72
+ {error: undefined, expectedMessage: undefined}
118
73
  ])(
119
- 'sets initFailed to true when collectPreauthCatalog errors but still sets ready to true',
74
+ 'sets initFailed to true when collectPreauthCatalog errors',
120
75
  async ({error, expectedMessage}) => {
121
76
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
122
77
  return Promise.reject(error);
@@ -124,18 +79,16 @@ describe('webex-core', () => {
124
79
 
125
80
  services.listenToOnce = sinon.stub();
126
81
  services.logger.error = sinon.stub();
127
- services.trigger = sinon.stub();
128
82
 
129
83
  services.initialize();
130
84
 
131
- // call the onLoaded callback
85
+ // call the onReady callback
86
+ services.listenToOnce.getCall(0).args[2]();
132
87
  services.listenToOnce.getCall(1).args[2]();
133
88
 
134
89
  await waitForAsync();
135
90
 
136
91
  assert.isTrue(services.initFailed);
137
- assert.isTrue(services.ready);
138
- sinon.assert.calledWith(services.trigger, 'services:initialized');
139
92
  sinon.assert.calledWith(
140
93
  services.logger.error,
141
94
  `services: failed to init initial services when no credentials available, ${expectedMessage}`
@@ -145,132 +98,329 @@ describe('webex-core', () => {
145
98
 
146
99
  it.each([
147
100
  {error: new Error('failed'), expectedMessage: 'failed'},
148
- {error: undefined, expectedMessage: undefined},
149
- ])(
150
- 'sets initFailed to true when initServiceCatalogs errors but still sets ready to true',
151
- async ({error, expectedMessage}) => {
152
- services.initServiceCatalogs = sinon.stub().callsFake(() => {
153
- return Promise.reject(error);
154
- });
155
- services.webex.credentials = {
156
- supertoken: {
157
- access_token: 'token',
158
- },
159
- };
101
+ {error: undefined, expectedMessage: undefined}
102
+ ])('sets initFailed to true when initServiceCatalogs errors', async ({error, expectedMessage}) => {
103
+ services.initServiceCatalogs = sinon.stub().callsFake(() => {
104
+ return Promise.reject(error);
105
+ });
106
+ services.webex.credentials = {
107
+ supertoken: {
108
+ access_token: 'token'
109
+ }
110
+ }
160
111
 
161
- services.listenToOnce = sinon.stub();
162
- services.logger.error = sinon.stub();
163
- services.trigger = sinon.stub();
112
+ services.listenToOnce = sinon.stub();
113
+ services.logger.error = sinon.stub();
164
114
 
165
- services.initialize();
115
+ services.initialize();
166
116
 
167
- // call the onLoaded callback
168
- services.listenToOnce.getCall(1).args[2]();
117
+ // call the onReady callback
118
+ services.listenToOnce.getCall(0).args[2]();
119
+ services.listenToOnce.getCall(1).args[2]();
169
120
 
170
- await waitForAsync();
121
+ await waitForAsync();
171
122
 
172
- assert.isTrue(services.initFailed);
173
- assert.isTrue(services.ready);
174
- sinon.assert.calledWith(services.trigger, 'services:initialized');
175
- sinon.assert.calledWith(
176
- services.logger.error,
177
- `services: failed to init initial services when credentials available, ${expectedMessage}`
178
- );
179
- }
180
- );
123
+ assert.isTrue(services.initFailed);
124
+ sinon.assert.calledWith(
125
+ services.logger.error,
126
+ `services: failed to init initial services when credentials available, ${expectedMessage}`
127
+ );
128
+ });
181
129
 
182
- describe('when change:canAuthorize fires', () => {
183
- it('calls initServiceCatalogs when canAuthorize becomes true and postauth catalog is not ready', async () => {
184
- services.listenToOnce = sinon.stub();
185
- services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(false));
186
- services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
187
- services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
188
- services.trigger = sinon.stub();
189
- // Ensure no credentials so we go to the preauth path
190
- services.webex.credentials = {};
130
+ it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
131
+ services.listenToOnce = sinon.stub();
132
+ services.initialize();
191
133
 
192
- services.initialize();
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]();
193
137
 
194
- // Get the catalog after initialize creates it
195
- const testCatalog = services._getCatalog();
196
- testCatalog.status.postauth.ready = false;
138
+ assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
139
+ });
197
140
 
198
- // call the onLoaded callback (preauth path)
199
- services.listenToOnce.getCall(1).args[2]();
200
- await waitForAsync();
141
+ it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
142
+ services.listenToOnce = sinon.stub();
143
+ services.initialize();
201
144
 
202
- // Now set canAuthorize to true to simulate auth completing
203
- services.webex.canAuthorize = true;
145
+ // Fire change:config to trigger the mode-specific listener registration.
146
+ services.listenToOnce.getCall(0).args[2]();
204
147
 
205
- // call the change:canAuthorize callback (should be call 2)
206
- services.listenToOnce.getCall(2).args[2]();
207
- await waitForAsync();
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
+ });
208
153
 
209
- sinon.assert.calledOnce(services.initServiceCatalogs);
210
- assert.isTrue(testCatalog.isReady);
211
- });
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
+ });
212
166
 
213
- it('does not call initServiceCatalogs when postauth catalog is already ready', async () => {
214
- services.listenToOnce = sinon.stub();
215
- services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(false));
216
- services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
217
- services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
218
- services.trigger = sinon.stub();
219
- // Ensure no credentials so we go to the preauth path
220
- services.webex.credentials = {};
167
+ it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
168
+ services.listenToOnce = sinon.stub();
169
+ services.initialize();
221
170
 
222
- services.initialize();
171
+ assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
172
+ });
223
173
 
224
- // Get the catalog after initialize creates it
225
- const testCatalog = services._getCatalog();
174
+ it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
175
+ services.listenToOnce = sinon.stub();
176
+ services.initialize();
226
177
 
227
- // call the onLoaded callback (preauth path)
228
- services.listenToOnce.getCall(1).args[2]();
229
- await waitForAsync();
178
+ // Fire change:config to trigger the mode-specific listener registration.
179
+ services.listenToOnce.getCall(0).args[2]();
230
180
 
231
- // Set canAuthorize to true and postauth as ready BEFORE calling the callback
232
- services.webex.canAuthorize = true;
233
- testCatalog.status.postauth.ready = true;
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
+ });
234
185
 
235
- // call the change:canAuthorize callback (should be call 2)
236
- services.listenToOnce.getCall(2).args[2]();
237
- await waitForAsync();
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
+ };
238
192
 
239
- sinon.assert.notCalled(services.initServiceCatalogs);
240
- });
193
+ services.initialize();
194
+ services.listenToOnce.getCall(0).args[2]();
195
+ services.listenToOnce.getCall(1).args[2]();
196
+ await waitForAsync();
241
197
 
242
- it('logs an error when initServiceCatalogs fails after auth', async () => {
243
- services.listenToOnce = sinon.stub();
244
- services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(false));
245
- services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
246
- services.initServiceCatalogs = sinon.stub().rejects(new Error('auth failed'));
247
- services.logger.error = sinon.stub();
248
- services.trigger = sinon.stub();
249
- // Ensure no credentials so we go to the preauth path
250
- services.webex.credentials = {};
198
+ assert.isTrue(services.ready);
199
+ });
251
200
 
252
- services.initialize();
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());
253
204
 
254
- // Get the catalog after initialize creates it
255
- const testCatalog = services._getCatalog();
256
- testCatalog.status.postauth.ready = false;
205
+ services.initialize();
206
+ services.listenToOnce.getCall(0).args[2]();
207
+ services.listenToOnce.getCall(1).args[2]();
208
+ await waitForAsync();
257
209
 
258
- // call the onLoaded callback (preauth path)
259
- services.listenToOnce.getCall(1).args[2]();
260
- await waitForAsync();
210
+ assert.isTrue(services.ready);
211
+ });
261
212
 
262
- // Now set canAuthorize to true to simulate auth completing
263
- services.webex.canAuthorize = true;
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();
264
220
 
265
- // call the change:canAuthorize callback (should be call 2)
266
- services.listenToOnce.getCall(2).args[2]();
267
- await waitForAsync();
221
+ services.initialize();
222
+ services.listenToOnce.getCall(0).args[2]();
223
+ services.listenToOnce.getCall(1).args[2]();
224
+ await waitForAsync();
268
225
 
269
- sinon.assert.calledWith(
270
- services.logger.error,
271
- 'services: failed to init service catalogs after auth, auth failed'
272
- );
273
- });
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);
274
424
  });
275
425
  });
276
426
 
@@ -309,7 +459,7 @@ describe('webex-core', () => {
309
459
 
310
460
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
311
461
  return Promise.resolve();
312
- });
462
+ })
313
463
 
314
464
  services.updateServices = sinon.stub().callsFake(() => {
315
465
  return Promise.reject(error);
@@ -404,7 +554,7 @@ describe('webex-core', () => {
404
554
  discovery: {
405
555
  sqdiscovery: 'https://test.ciscospark.com/v1/region',
406
556
  },
407
- },
557
+ }
408
558
  };
409
559
  });
410
560
 
@@ -424,8 +574,8 @@ describe('webex-core', () => {
424
574
  assert.calledWith(webex.request, {
425
575
  uri: 'https://test.ciscospark.com/v1/region',
426
576
  addAuthHeader: false,
427
- headers: {'spark-user-agent': null},
428
- timeout: 5000,
577
+ headers: { 'spark-user-agent': null },
578
+ timeout: 5000
429
579
  });
430
580
  });
431
581
  });
@@ -482,6 +632,7 @@ describe('webex-core', () => {
482
632
  });
483
633
 
484
634
  describe('#_fetchNewServiceHostmap()', () => {
635
+
485
636
  beforeEach(() => {
486
637
  sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
487
638
  });
@@ -495,20 +646,17 @@ describe('webex-core', () => {
495
646
 
496
647
  sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
497
648
  sinon.stub(services, 'request').resolves({});
498
-
649
+
499
650
  const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
500
651
 
501
652
  assert.calledOnceWithExactly(services.request, {
502
653
  method: 'GET',
503
654
  service: 'u2c',
504
655
  resource: '/limited/catalog',
505
- qs: {format: 'hostmap'},
506
- });
507
- assert.calledOnceWithExactly(
508
- webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
509
- sinon.match.func,
510
- 'internal.get.u2c.time'
656
+ qs: {format: 'hostmap'}
657
+ }
511
658
  );
659
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
512
660
  });
513
661
 
514
662
  it('checks service request rejects', async () => {
@@ -516,7 +664,7 @@ describe('webex-core', () => {
516
664
 
517
665
  sinon.spy(services, '_formatReceivedHostmap');
518
666
  sinon.stub(services, 'request').rejects(error);
519
-
667
+
520
668
  const promise = services._fetchNewServiceHostmap({from: 'limited'});
521
669
  const rejectedValue = await assert.isRejected(promise);
522
670
 
@@ -528,13 +676,10 @@ describe('webex-core', () => {
528
676
  method: 'GET',
529
677
  service: 'u2c',
530
678
  resource: '/limited/catalog',
531
- qs: {format: 'hostmap'},
532
- });
533
- assert.calledOnceWithExactly(
534
- webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
535
- sinon.match.func,
536
- 'internal.get.u2c.time'
679
+ qs: {format: 'hostmap'}
680
+ }
537
681
  );
682
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
538
683
  });
539
684
  });
540
685
 
@@ -565,6 +710,7 @@ describe('webex-core', () => {
565
710
  });
566
711
 
567
712
  it('returns the original uri if the hostmap has no hosts for the host', () => {
713
+
568
714
  services._hostCatalog = {
569
715
  'example.com': [],
570
716
  };
@@ -728,7 +874,8 @@ describe('webex-core', () => {
728
874
  id: '0:0:0:different-e-x',
729
875
  },
730
876
  ],
731
- 'example-f.com': [],
877
+ 'example-f.com': [
878
+ ],
732
879
  },
733
880
  format: 'hostmap',
734
881
  };
@@ -936,7 +1083,7 @@ describe('webex-core', () => {
936
1083
  defaultUrl: 'https://example-g.com/api/v1',
937
1084
  hosts: [],
938
1085
  name: 'example-g',
939
- },
1086
+ }
940
1087
  ]);
941
1088
  });
942
1089
 
@@ -990,59 +1137,34 @@ describe('webex-core', () => {
990
1137
  assert.equal(webex.config.credentials.authorizeUrl, authUrl);
991
1138
  });
992
1139
  });
993
-
1140
+
994
1141
  describe('#getMobiusClusters', () => {
995
1142
  it('returns unique mobius host entries from hostCatalog', () => {
996
1143
  // Arrange: two hostCatalog keys, with duplicate mobius host across keys
997
1144
  services._hostCatalog = {
998
1145
  'mobius-us-east-2.prod.infra.webex.com': [
999
- {
1000
- host: 'mobius-us-east-2.prod.infra.webex.com',
1001
- ttl: -1,
1002
- priority: 5,
1003
- id: 'urn:TEAM:xyz:mobius',
1004
- },
1005
- {
1006
- host: 'mobius-eu-central-1.prod.infra.webex.com',
1007
- ttl: -1,
1008
- priority: 10,
1009
- id: 'urn:TEAM:xyz:mobius',
1010
- },
1011
- ],
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
+ ],
1012
1149
 
1013
1150
  'mobius-eu-central-1.prod.infra.webex.com': [
1014
- {
1015
- host: 'mobius-us-east-2.prod.infra.webex.com',
1016
- ttl: -1,
1017
- priority: 7,
1018
- id: 'urn:TEAM:xyz:mobius',
1019
- }, // duplicate host
1020
- ],
1021
- 'wdm-a.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' : [
1022
1154
  {host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
1023
- ],
1155
+ ]
1024
1156
  };
1025
-
1157
+
1026
1158
  // Act
1027
1159
  const clusters = services.getMobiusClusters();
1028
-
1160
+
1029
1161
  // Assert
1030
1162
  // deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
1031
1163
  assert.deepEqual(
1032
1164
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
1033
1165
  [
1034
- {
1035
- host: 'mobius-us-east-2.prod.infra.webex.com',
1036
- id: 'urn:TEAM:xyz:mobius',
1037
- ttl: -1,
1038
- priority: 5,
1039
- },
1040
- {
1041
- host: 'mobius-eu-central-1.prod.infra.webex.com',
1042
- id: 'urn:TEAM:xyz:mobius',
1043
- ttl: -1,
1044
- priority: 10,
1045
- },
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},
1046
1168
  ]
1047
1169
  );
1048
1170
  });
@@ -1051,31 +1173,31 @@ describe('webex-core', () => {
1051
1173
  describe('#isValidHost', () => {
1052
1174
  beforeEach(() => {
1053
1175
  // Setting up a mock host catalog
1054
- services._hostCatalog = {
1055
- 'audit-ci-m.wbx2.com': [
1056
- {
1057
- host: 'audit-ci-m.wbx2.com',
1058
- ttl: -1,
1059
- priority: 5,
1060
- id: 'urn:IDENTITY:PA61:adminAudit',
1061
- },
1062
- {
1063
- host: 'audit-ci-m.wbx2.com',
1064
- ttl: -1,
1065
- priority: 5,
1066
- id: 'urn:IDENTITY:PA61:adminAuditV2',
1067
- },
1068
- ],
1069
- 'mercury-connection-partition0-r.wbx2.com': [
1070
- {
1071
- host: 'mercury-connection-partition0-r.wbx2.com',
1072
- ttl: -1,
1073
- priority: 5,
1074
- id: 'urn:TEAM:us-west-2_r:mercuryConnectionPartition0',
1075
- },
1076
- ],
1077
- 'empty.com': [],
1078
- };
1176
+ services._hostCatalog = {
1177
+ "audit-ci-m.wbx2.com": [
1178
+ {
1179
+ "host": "audit-ci-m.wbx2.com",
1180
+ "ttl": -1,
1181
+ "priority": 5,
1182
+ "id": "urn:IDENTITY:PA61:adminAudit"
1183
+ },
1184
+ {
1185
+ "host": "audit-ci-m.wbx2.com",
1186
+ "ttl": -1,
1187
+ "priority": 5,
1188
+ "id": "urn:IDENTITY:PA61:adminAuditV2"
1189
+ }
1190
+ ],
1191
+ "mercury-connection-partition0-r.wbx2.com": [
1192
+ {
1193
+ "host": "mercury-connection-partition0-r.wbx2.com",
1194
+ "ttl": -1,
1195
+ "priority": 5,
1196
+ "id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
1197
+ }
1198
+ ],
1199
+ "empty.com": []
1200
+ };
1079
1201
  });
1080
1202
  afterAll(() => {
1081
1203
  // Clean up the mock host catalog
@@ -1157,7 +1279,7 @@ describe('webex-core', () => {
1157
1279
  let catalog;
1158
1280
  let localStorageBackup;
1159
1281
  let windowBackup;
1160
-
1282
+
1161
1283
  const makeLocalStorageShim = () => {
1162
1284
  const store = new Map();
1163
1285
  return {
@@ -1167,16 +1289,13 @@ describe('webex-core', () => {
1167
1289
  _store: store,
1168
1290
  };
1169
1291
  };
1170
-
1292
+
1171
1293
  beforeEach(() => {
1172
1294
  // Build a fresh webex instance
1173
- webex = new MockWebex({
1174
- children: {services: Services},
1175
- config: {credentials: {federation: true}},
1176
- });
1295
+ webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
1177
1296
  services = webex.internal.services;
1178
1297
  catalog = services._getCatalog();
1179
-
1298
+
1180
1299
  // enable U2C caching feature flag in tests that rely on localStorage writes/reads
1181
1300
  services.webex.config = services.webex.config || {};
1182
1301
  services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
@@ -1188,15 +1307,13 @@ describe('webex-core', () => {
1188
1307
  global.window.localStorage = makeLocalStorageShim();
1189
1308
  // Ensure code under test uses our shim via util method
1190
1309
  sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
1191
-
1310
+
1192
1311
  // Stub the formatter so we don't need a full hostmap payload in tests
1193
- sinon
1194
- .stub(services, '_formatReceivedHostmap')
1195
- .callsFake(() => [
1196
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1197
- ]);
1312
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1313
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1314
+ ]);
1198
1315
  });
1199
-
1316
+
1200
1317
  afterEach(() => {
1201
1318
  global.window.localStorage = localStorageBackup || undefined;
1202
1319
  if (!windowBackup) {
@@ -1209,7 +1326,7 @@ describe('webex-core', () => {
1209
1326
  services._getLocalStorageSafe.restore();
1210
1327
  }
1211
1328
  });
1212
-
1329
+
1213
1330
  it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
1214
1331
  // Arrange: authenticated credentials and spies
1215
1332
  services.webex.credentials = {
@@ -1221,20 +1338,10 @@ describe('webex-core', () => {
1221
1338
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1222
1339
  const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
1223
1340
  // Make fetch return a hostmap object and allow formatter to reduce it
1224
- sinon
1225
- .stub(services, 'request')
1226
- .resolves({
1227
- body: {
1228
- services: [],
1229
- activeServices: {},
1230
- timestamp: Date.now().toString(),
1231
- orgId: 'urn:EXAMPLE:org',
1232
- format: 'U2CV2',
1233
- },
1234
- });
1235
- // Cause loaded callback to run immediately
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
1236
1343
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1237
- if (event === 'loaded') cb();
1344
+ if (event === 'change:config' || event === 'ready') cb();
1238
1345
  });
1239
1346
 
1240
1347
  // Act
@@ -1274,9 +1381,9 @@ describe('webex-core', () => {
1274
1381
 
1275
1382
  const initSpy = sinon.spy(services, 'initServiceCatalogs');
1276
1383
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1277
- // Cause loaded callback to run immediately
1384
+ // Cause ready callback to run immediately
1278
1385
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1279
- if (event === 'loaded') cb();
1386
+ if (event === 'change:config' || event === 'ready') cb();
1280
1387
  });
1281
1388
 
1282
1389
  // Act
@@ -1284,18 +1391,9 @@ describe('webex-core', () => {
1284
1391
  await waitForAsync();
1285
1392
 
1286
1393
  // Assert: ready path found cache and skipped initServiceCatalogs
1287
- assert.isFalse(
1288
- initSpy.called,
1289
- 'expected initServiceCatalogs to be skipped with cache present'
1290
- );
1291
- assert.isTrue(
1292
- services._getCatalog().status.preauth.ready,
1293
- 'preauth should be ready from cache'
1294
- );
1295
- assert.isTrue(
1296
- services._getCatalog().status.postauth.ready,
1297
- 'postauth should be ready from cache'
1298
- );
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');
1299
1397
  assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
1300
1398
 
1301
1399
  // Cleanup
@@ -1311,29 +1409,26 @@ describe('webex-core', () => {
1311
1409
  preauth: {serviceLinks: {}, hostCatalog: {}},
1312
1410
  postauth: {serviceLinks: {}, hostCatalog: {}},
1313
1411
  };
1314
-
1412
+
1315
1413
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
1316
-
1414
+
1317
1415
  const warmed = await services._loadCatalogFromCache();
1318
-
1416
+
1319
1417
  assert.isFalse(warmed, 'stale cache must not warm');
1320
- assert.isNull(
1321
- window.localStorage.getItem(CATALOG_CACHE_KEY_V1),
1322
- 'expired cache must be cleared'
1323
- );
1418
+ assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
1324
1419
  assert.isFalse(catalog.status.preauth.ready);
1325
1420
  assert.isFalse(catalog.status.postauth.ready);
1326
1421
  });
1327
-
1422
+
1328
1423
  it('clearCatalogCache() removes the cached entry', async () => {
1329
1424
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1330
1425
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
1331
-
1426
+
1332
1427
  await services.clearCatalogCache();
1333
-
1428
+
1334
1429
  assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
1335
1430
  });
1336
-
1431
+
1337
1432
  it('still fetches when forceRefresh=true even if ready', async () => {
1338
1433
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1339
1434
  window.localStorage.setItem(
@@ -1345,24 +1440,20 @@ describe('webex-core', () => {
1345
1440
  postauth: {serviceLinks: {}, hostCatalog: {}},
1346
1441
  })
1347
1442
  );
1348
-
1443
+
1349
1444
  // warm from cache
1350
1445
  const warmed = await services._loadCatalogFromCache();
1351
1446
  assert.isTrue(warmed);
1352
1447
  assert.isTrue(catalog.status.preauth.ready);
1353
1448
  assert.isTrue(catalog.status.postauth.ready);
1354
-
1449
+
1355
1450
  const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
1356
-
1451
+
1357
1452
  // with forceRefresh we should fetch despite ready=true
1358
- await services.updateServices({
1359
- from: 'limited',
1360
- query: {orgId: 'urn:EXAMPLE:org'},
1361
- forceRefresh: true,
1362
- });
1453
+ await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
1363
1454
  // pass an empty query to avoid spreading undefined in qs construction
1364
1455
  await services.updateServices({forceRefresh: true});
1365
-
1456
+
1366
1457
  assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
1367
1458
  fetchSpy.restore();
1368
1459
  });
@@ -1423,11 +1514,9 @@ describe('webex-core', () => {
1423
1514
  );
1424
1515
  // formatter returns at least one entry to mark ready
1425
1516
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1426
- sinon
1427
- .stub(services, '_formatReceivedHostmap')
1428
- .callsFake(() => [
1429
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1430
- ]);
1517
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1518
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1519
+ ]);
1431
1520
 
1432
1521
  const warmed = await services._loadCatalogFromCache();
1433
1522
  assert.isTrue(warmed);
@@ -1449,11 +1538,9 @@ describe('webex-core', () => {
1449
1538
  })
1450
1539
  );
1451
1540
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1452
- sinon
1453
- .stub(services, '_formatReceivedHostmap')
1454
- .callsFake(() => [
1455
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1456
- ]);
1541
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1542
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1543
+ ]);
1457
1544
 
1458
1545
  const warmed = await services._loadCatalogFromCache();
1459
1546
  // function returns true if overall cache path succeeded; we only verify group readiness
@@ -1480,17 +1567,12 @@ describe('webex-core', () => {
1480
1567
  })
1481
1568
  );
1482
1569
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1483
- sinon
1484
- .stub(services, '_formatReceivedHostmap')
1485
- .callsFake(() => [
1486
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1487
- ]);
1570
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1571
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1572
+ ]);
1488
1573
 
1489
1574
  await services._loadCatalogFromCache();
1490
- assert.isFalse(
1491
- catalog.status.preauth.ready,
1492
- 'preauth should not warm on selection mismatch'
1493
- );
1575
+ assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
1494
1576
  });
1495
1577
 
1496
1578
  it('skips warming when environment fingerprint mismatches', async () => {
@@ -1501,10 +1583,7 @@ describe('webex-core', () => {
1501
1583
  JSON.stringify({
1502
1584
  cachedAt: Date.now(),
1503
1585
  env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
1504
- preauth: {
1505
- hostMap: {serviceLinks: {}, hostCatalog: {}},
1506
- meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
1507
- },
1586
+ preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
1508
1587
  })
1509
1588
  );
1510
1589
  // current env
@@ -1519,6 +1598,7 @@ describe('webex-core', () => {
1519
1598
  assert.isFalse(catalog.status.postauth.ready);
1520
1599
  });
1521
1600
  });
1601
+
1522
1602
  });
1523
1603
  });
1524
1604
  /* eslint-enable no-underscore-dangle */