@webex/webex-core 3.12.0-webex-services-ready.1 → 3.12.0-webex-services-ready.3

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,338 @@ 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
+ // After the postauth catalog init resolves, catalog.isReady must flip
330
+ // to true so downstream consumers see the catalog as usable.
331
+ await waitForAsync();
332
+
333
+ assert.isTrue(
334
+ currentCatalog.isReady,
335
+ 'expected catalog.isReady=true after postauth catalog init succeeds'
336
+ );
337
+ });
338
+
339
+ it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
340
+ services.listenToOnce = sinon.stub();
341
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
342
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
343
+
344
+ services.initialize();
345
+ // initialize() creates a new catalog; grab the current one.
346
+ const currentCatalog = services._getCatalog();
347
+ services.listenToOnce.getCall(0).args[2]();
348
+ services.listenToOnce.getCall(1).args[2]();
349
+ await waitForAsync();
350
+
351
+ const freshLoginCall = services.listenToOnce
352
+ .getCalls()
353
+ .find((call) => call.args[1] === 'change:canAuthorize');
354
+
355
+ services.webex.canAuthorize = true;
356
+ currentCatalog.status.postauth.ready = true; // already collected
357
+ freshLoginCall.args[2]();
358
+
359
+ assert.isFalse(
360
+ services.initServiceCatalogs.called,
361
+ 'must not re-fetch postauth catalog if it is already ready'
362
+ );
363
+ });
364
+
365
+ it('finalizes ready when cache is warm (skips network fetch)', async () => {
366
+ services.listenToOnce = sinon.stub();
367
+ services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
368
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
369
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
370
+
371
+ services.initialize();
372
+ services.listenToOnce.getCall(0).args[2]();
373
+ services.listenToOnce.getCall(1).args[2]();
374
+ await waitForAsync();
375
+ await waitForAsync();
376
+
377
+ assert.isTrue(services.ready);
378
+ assert.isFalse(
379
+ services.initServiceCatalogs.called,
380
+ 'must skip network fetch when cache is warm'
381
+ );
382
+ assert.isFalse(
383
+ services.collectPreauthCatalog.called,
384
+ 'must skip preauth fetch when cache is warm'
385
+ );
386
+ });
387
+ });
388
+
389
+ describe('#_finalizeReady', () => {
390
+ beforeEach(() => {
391
+ // Outer beforeEach construction already ran initialize() (ungated,
392
+ // sets ready=true). Reset so _finalizeReady's flip is observable.
393
+ services.ready = false;
394
+ });
395
+
396
+ it('sets ready=true immediately when credentials are not refreshing', async () => {
397
+ services.webex.credentials = {isRefreshing: false};
398
+ assert.isFalse(services.ready);
399
+
400
+ await services._finalizeReady();
401
+
402
+ assert.isTrue(services.ready);
403
+ });
404
+
405
+ it('sets ready=true when credentials are missing', async () => {
406
+ services.webex.credentials = undefined;
407
+
408
+ await services._finalizeReady();
409
+
410
+ assert.isTrue(services.ready);
411
+ });
412
+
413
+ it('awaits change:isRefreshing before setting ready=true', async () => {
414
+ let onChangeIsRefreshing;
415
+ services.webex.credentials = {
416
+ isRefreshing: true,
417
+ once: sinon.stub().callsFake((event, cb) => {
418
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
419
+ }),
420
+ };
421
+
422
+ const settled = services._finalizeReady();
423
+ // Let the async body reach its await point.
424
+ await waitForAsync();
425
+
426
+ assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
427
+ assert.isFunction(onChangeIsRefreshing);
428
+
429
+ onChangeIsRefreshing();
430
+ await settled;
431
+
432
+ assert.isTrue(services.ready);
274
433
  });
275
434
  });
276
435
 
@@ -309,7 +468,7 @@ describe('webex-core', () => {
309
468
 
310
469
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
311
470
  return Promise.resolve();
312
- });
471
+ })
313
472
 
314
473
  services.updateServices = sinon.stub().callsFake(() => {
315
474
  return Promise.reject(error);
@@ -404,7 +563,7 @@ describe('webex-core', () => {
404
563
  discovery: {
405
564
  sqdiscovery: 'https://test.ciscospark.com/v1/region',
406
565
  },
407
- },
566
+ }
408
567
  };
409
568
  });
410
569
 
@@ -424,8 +583,8 @@ describe('webex-core', () => {
424
583
  assert.calledWith(webex.request, {
425
584
  uri: 'https://test.ciscospark.com/v1/region',
426
585
  addAuthHeader: false,
427
- headers: {'spark-user-agent': null},
428
- timeout: 5000,
586
+ headers: { 'spark-user-agent': null },
587
+ timeout: 5000
429
588
  });
430
589
  });
431
590
  });
@@ -482,6 +641,7 @@ describe('webex-core', () => {
482
641
  });
483
642
 
484
643
  describe('#_fetchNewServiceHostmap()', () => {
644
+
485
645
  beforeEach(() => {
486
646
  sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
487
647
  });
@@ -495,20 +655,17 @@ describe('webex-core', () => {
495
655
 
496
656
  sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
497
657
  sinon.stub(services, 'request').resolves({});
498
-
658
+
499
659
  const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
500
660
 
501
661
  assert.calledOnceWithExactly(services.request, {
502
662
  method: 'GET',
503
663
  service: 'u2c',
504
664
  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'
665
+ qs: {format: 'hostmap'}
666
+ }
511
667
  );
668
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
512
669
  });
513
670
 
514
671
  it('checks service request rejects', async () => {
@@ -516,7 +673,7 @@ describe('webex-core', () => {
516
673
 
517
674
  sinon.spy(services, '_formatReceivedHostmap');
518
675
  sinon.stub(services, 'request').rejects(error);
519
-
676
+
520
677
  const promise = services._fetchNewServiceHostmap({from: 'limited'});
521
678
  const rejectedValue = await assert.isRejected(promise);
522
679
 
@@ -528,13 +685,10 @@ describe('webex-core', () => {
528
685
  method: 'GET',
529
686
  service: 'u2c',
530
687
  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'
688
+ qs: {format: 'hostmap'}
689
+ }
537
690
  );
691
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
538
692
  });
539
693
  });
540
694
 
@@ -565,6 +719,7 @@ describe('webex-core', () => {
565
719
  });
566
720
 
567
721
  it('returns the original uri if the hostmap has no hosts for the host', () => {
722
+
568
723
  services._hostCatalog = {
569
724
  'example.com': [],
570
725
  };
@@ -728,7 +883,8 @@ describe('webex-core', () => {
728
883
  id: '0:0:0:different-e-x',
729
884
  },
730
885
  ],
731
- 'example-f.com': [],
886
+ 'example-f.com': [
887
+ ],
732
888
  },
733
889
  format: 'hostmap',
734
890
  };
@@ -936,7 +1092,7 @@ describe('webex-core', () => {
936
1092
  defaultUrl: 'https://example-g.com/api/v1',
937
1093
  hosts: [],
938
1094
  name: 'example-g',
939
- },
1095
+ }
940
1096
  ]);
941
1097
  });
942
1098
 
@@ -990,59 +1146,34 @@ describe('webex-core', () => {
990
1146
  assert.equal(webex.config.credentials.authorizeUrl, authUrl);
991
1147
  });
992
1148
  });
993
-
1149
+
994
1150
  describe('#getMobiusClusters', () => {
995
1151
  it('returns unique mobius host entries from hostCatalog', () => {
996
1152
  // Arrange: two hostCatalog keys, with duplicate mobius host across keys
997
1153
  services._hostCatalog = {
998
1154
  '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
- ],
1155
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
1156
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
1157
+ ],
1012
1158
 
1013
1159
  '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': [
1160
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
1161
+ ],
1162
+ 'wdm-a.webex.com' : [
1022
1163
  {host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
1023
- ],
1164
+ ]
1024
1165
  };
1025
-
1166
+
1026
1167
  // Act
1027
1168
  const clusters = services.getMobiusClusters();
1028
-
1169
+
1029
1170
  // Assert
1030
1171
  // deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
1031
1172
  assert.deepEqual(
1032
1173
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
1033
1174
  [
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
- },
1175
+ {host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
1176
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
1046
1177
  ]
1047
1178
  );
1048
1179
  });
@@ -1051,31 +1182,31 @@ describe('webex-core', () => {
1051
1182
  describe('#isValidHost', () => {
1052
1183
  beforeEach(() => {
1053
1184
  // 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
- };
1185
+ services._hostCatalog = {
1186
+ "audit-ci-m.wbx2.com": [
1187
+ {
1188
+ "host": "audit-ci-m.wbx2.com",
1189
+ "ttl": -1,
1190
+ "priority": 5,
1191
+ "id": "urn:IDENTITY:PA61:adminAudit"
1192
+ },
1193
+ {
1194
+ "host": "audit-ci-m.wbx2.com",
1195
+ "ttl": -1,
1196
+ "priority": 5,
1197
+ "id": "urn:IDENTITY:PA61:adminAuditV2"
1198
+ }
1199
+ ],
1200
+ "mercury-connection-partition0-r.wbx2.com": [
1201
+ {
1202
+ "host": "mercury-connection-partition0-r.wbx2.com",
1203
+ "ttl": -1,
1204
+ "priority": 5,
1205
+ "id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
1206
+ }
1207
+ ],
1208
+ "empty.com": []
1209
+ };
1079
1210
  });
1080
1211
  afterAll(() => {
1081
1212
  // Clean up the mock host catalog
@@ -1157,7 +1288,7 @@ describe('webex-core', () => {
1157
1288
  let catalog;
1158
1289
  let localStorageBackup;
1159
1290
  let windowBackup;
1160
-
1291
+
1161
1292
  const makeLocalStorageShim = () => {
1162
1293
  const store = new Map();
1163
1294
  return {
@@ -1167,16 +1298,13 @@ describe('webex-core', () => {
1167
1298
  _store: store,
1168
1299
  };
1169
1300
  };
1170
-
1301
+
1171
1302
  beforeEach(() => {
1172
1303
  // Build a fresh webex instance
1173
- webex = new MockWebex({
1174
- children: {services: Services},
1175
- config: {credentials: {federation: true}},
1176
- });
1304
+ webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
1177
1305
  services = webex.internal.services;
1178
1306
  catalog = services._getCatalog();
1179
-
1307
+
1180
1308
  // enable U2C caching feature flag in tests that rely on localStorage writes/reads
1181
1309
  services.webex.config = services.webex.config || {};
1182
1310
  services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
@@ -1188,15 +1316,13 @@ describe('webex-core', () => {
1188
1316
  global.window.localStorage = makeLocalStorageShim();
1189
1317
  // Ensure code under test uses our shim via util method
1190
1318
  sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
1191
-
1319
+
1192
1320
  // 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
- ]);
1321
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1322
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1323
+ ]);
1198
1324
  });
1199
-
1325
+
1200
1326
  afterEach(() => {
1201
1327
  global.window.localStorage = localStorageBackup || undefined;
1202
1328
  if (!windowBackup) {
@@ -1209,7 +1335,7 @@ describe('webex-core', () => {
1209
1335
  services._getLocalStorageSafe.restore();
1210
1336
  }
1211
1337
  });
1212
-
1338
+
1213
1339
  it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
1214
1340
  // Arrange: authenticated credentials and spies
1215
1341
  services.webex.credentials = {
@@ -1221,20 +1347,10 @@ describe('webex-core', () => {
1221
1347
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1222
1348
  const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
1223
1349
  // 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
1350
+ sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
1351
+ // Cause ready callback to run immediately
1236
1352
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1237
- if (event === 'loaded') cb();
1353
+ if (event === 'change:config' || event === 'ready') cb();
1238
1354
  });
1239
1355
 
1240
1356
  // Act
@@ -1274,9 +1390,9 @@ describe('webex-core', () => {
1274
1390
 
1275
1391
  const initSpy = sinon.spy(services, 'initServiceCatalogs');
1276
1392
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1277
- // Cause loaded callback to run immediately
1393
+ // Cause ready callback to run immediately
1278
1394
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1279
- if (event === 'loaded') cb();
1395
+ if (event === 'change:config' || event === 'ready') cb();
1280
1396
  });
1281
1397
 
1282
1398
  // Act
@@ -1284,18 +1400,9 @@ describe('webex-core', () => {
1284
1400
  await waitForAsync();
1285
1401
 
1286
1402
  // 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
- );
1403
+ assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
1404
+ assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
1405
+ assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
1299
1406
  assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
1300
1407
 
1301
1408
  // Cleanup
@@ -1311,29 +1418,26 @@ describe('webex-core', () => {
1311
1418
  preauth: {serviceLinks: {}, hostCatalog: {}},
1312
1419
  postauth: {serviceLinks: {}, hostCatalog: {}},
1313
1420
  };
1314
-
1421
+
1315
1422
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
1316
-
1423
+
1317
1424
  const warmed = await services._loadCatalogFromCache();
1318
-
1425
+
1319
1426
  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
- );
1427
+ assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
1324
1428
  assert.isFalse(catalog.status.preauth.ready);
1325
1429
  assert.isFalse(catalog.status.postauth.ready);
1326
1430
  });
1327
-
1431
+
1328
1432
  it('clearCatalogCache() removes the cached entry', async () => {
1329
1433
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1330
1434
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
1331
-
1435
+
1332
1436
  await services.clearCatalogCache();
1333
-
1437
+
1334
1438
  assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
1335
1439
  });
1336
-
1440
+
1337
1441
  it('still fetches when forceRefresh=true even if ready', async () => {
1338
1442
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1339
1443
  window.localStorage.setItem(
@@ -1345,24 +1449,20 @@ describe('webex-core', () => {
1345
1449
  postauth: {serviceLinks: {}, hostCatalog: {}},
1346
1450
  })
1347
1451
  );
1348
-
1452
+
1349
1453
  // warm from cache
1350
1454
  const warmed = await services._loadCatalogFromCache();
1351
1455
  assert.isTrue(warmed);
1352
1456
  assert.isTrue(catalog.status.preauth.ready);
1353
1457
  assert.isTrue(catalog.status.postauth.ready);
1354
-
1458
+
1355
1459
  const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
1356
-
1460
+
1357
1461
  // 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
- });
1462
+ await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
1363
1463
  // pass an empty query to avoid spreading undefined in qs construction
1364
1464
  await services.updateServices({forceRefresh: true});
1365
-
1465
+
1366
1466
  assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
1367
1467
  fetchSpy.restore();
1368
1468
  });
@@ -1423,11 +1523,9 @@ describe('webex-core', () => {
1423
1523
  );
1424
1524
  // formatter returns at least one entry to mark ready
1425
1525
  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
- ]);
1526
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1527
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1528
+ ]);
1431
1529
 
1432
1530
  const warmed = await services._loadCatalogFromCache();
1433
1531
  assert.isTrue(warmed);
@@ -1449,11 +1547,9 @@ describe('webex-core', () => {
1449
1547
  })
1450
1548
  );
1451
1549
  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
- ]);
1550
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1551
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1552
+ ]);
1457
1553
 
1458
1554
  const warmed = await services._loadCatalogFromCache();
1459
1555
  // function returns true if overall cache path succeeded; we only verify group readiness
@@ -1480,17 +1576,12 @@ describe('webex-core', () => {
1480
1576
  })
1481
1577
  );
1482
1578
  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
- ]);
1579
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1580
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1581
+ ]);
1488
1582
 
1489
1583
  await services._loadCatalogFromCache();
1490
- assert.isFalse(
1491
- catalog.status.preauth.ready,
1492
- 'preauth should not warm on selection mismatch'
1493
- );
1584
+ assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
1494
1585
  });
1495
1586
 
1496
1587
  it('skips warming when environment fingerprint mismatches', async () => {
@@ -1501,10 +1592,7 @@ describe('webex-core', () => {
1501
1592
  JSON.stringify({
1502
1593
  cachedAt: Date.now(),
1503
1594
  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
- },
1595
+ preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
1508
1596
  })
1509
1597
  );
1510
1598
  // current env
@@ -1519,6 +1607,7 @@ describe('webex-core', () => {
1519
1607
  assert.isFalse(catalog.status.postauth.ready);
1520
1608
  });
1521
1609
  });
1610
+
1522
1611
  });
1523
1612
  });
1524
1613
  /* eslint-enable no-underscore-dangle */