@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-auth-prejoin-fetch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +24 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/credentials-config.js +12 -0
  5. package/dist/credentials-config.js.map +1 -1
  6. package/dist/interceptors/redirect.js +1 -1
  7. package/dist/interceptors/redirect.js.map +1 -1
  8. package/dist/lib/batcher.js +23 -7
  9. package/dist/lib/batcher.js.map +1 -1
  10. package/dist/lib/credentials/credentials.js +48 -4
  11. package/dist/lib/credentials/credentials.js.map +1 -1
  12. package/dist/lib/credentials/token.js +1 -1
  13. package/dist/lib/domains.js +90 -0
  14. package/dist/lib/domains.js.map +1 -0
  15. package/dist/lib/services/service-catalog.js +6 -10
  16. package/dist/lib/services/service-catalog.js.map +1 -1
  17. package/dist/lib/services/services.js +244 -57
  18. package/dist/lib/services/services.js.map +1 -1
  19. package/dist/lib/services-v2/service-catalog.js +6 -12
  20. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  21. package/dist/lib/services-v2/services-v2.js +243 -52
  22. package/dist/lib/services-v2/services-v2.js.map +1 -1
  23. package/dist/plugins/logger.js +1 -1
  24. package/dist/webex-core.js +2 -2
  25. package/dist/webex-core.js.map +1 -1
  26. package/package.json +13 -13
  27. package/src/config.js +27 -0
  28. package/src/credentials-config.js +13 -0
  29. package/src/interceptors/redirect.js +4 -1
  30. package/src/lib/batcher.js +25 -10
  31. package/src/lib/credentials/credentials.js +50 -3
  32. package/src/lib/domains.ts +94 -0
  33. package/src/lib/services/service-catalog.js +6 -10
  34. package/src/lib/services/services.js +218 -46
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +218 -42
  37. package/test/fixtures/activation-email.ts +22 -0
  38. package/test/integration/spec/services/service-catalog.js +7 -6
  39. package/test/integration/spec/services/services.js +188 -128
  40. package/test/integration/spec/services-v2/services-v2.js +174 -114
  41. package/test/unit/spec/credentials/credentials.js +133 -2
  42. package/test/unit/spec/interceptors/auth.js +56 -0
  43. package/test/unit/spec/lib/batcher.js +56 -0
  44. package/test/unit/spec/services/service-catalog.js +93 -11
  45. package/test/unit/spec/services/services.js +591 -322
  46. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  47. package/test/unit/spec/services-v2/services-v2.ts +539 -214
  48. package/test/unit/spec/webex-core.js +0 -2
  49. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -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,467 @@ 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
+ });
208
152
 
209
- sinon.assert.calledOnce(services.initServiceCatalogs);
210
- assert.isTrue(testCatalog.isReady);
211
- });
153
+ it('skips preauth catalog collection when skipPreauthCatalogOnUnauthenticated is true and no credentials', async () => {
154
+ services.webex.config = {
155
+ ...(services.webex.config || {}),
156
+ services: {skipPreauthCatalogOnUnauthenticated: true},
157
+ };
158
+ services.listenToOnce = sinon.stub();
159
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
160
+ services.logger.info = sinon.stub();
212
161
 
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 = {};
162
+ services.initialize();
163
+ services.listenToOnce.getCall(0).args[2]();
164
+ services.listenToOnce.getCall(1).args[2]();
165
+ await waitForAsync();
221
166
 
222
- services.initialize();
167
+ assert.isFalse(
168
+ services.collectPreauthCatalog.called,
169
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
170
+ );
171
+ assert.isFalse(services.initFailed);
172
+ sinon.assert.calledWith(
173
+ services.logger.info,
174
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
175
+ );
176
+ });
223
177
 
224
- // Get the catalog after initialize creates it
225
- const testCatalog = services._getCatalog();
178
+ it('still collects preauth catalog when skipPreauthCatalogOnUnauthenticated is false (default)', async () => {
179
+ services.webex.config = {
180
+ ...(services.webex.config || {}),
181
+ services: {skipPreauthCatalogOnUnauthenticated: false},
182
+ };
183
+ services.listenToOnce = sinon.stub();
184
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
226
185
 
227
- // call the onLoaded callback (preauth path)
228
- services.listenToOnce.getCall(1).args[2]();
229
- await waitForAsync();
186
+ services.initialize();
187
+ services.listenToOnce.getCall(0).args[2]();
188
+ services.listenToOnce.getCall(1).args[2]();
189
+ await waitForAsync();
230
190
 
231
- // Set canAuthorize to true and postauth as ready BEFORE calling the callback
232
- services.webex.canAuthorize = true;
233
- testCatalog.status.postauth.ready = true;
191
+ assert.isTrue(
192
+ services.collectPreauthCatalog.called,
193
+ 'must collect preauth catalog when option is disabled'
194
+ );
195
+ });
196
+ });
234
197
 
235
- // call the change:canAuthorize callback (should be call 2)
236
- services.listenToOnce.getCall(2).args[2]();
237
- await waitForAsync();
198
+ describe('#initialize (waitForCatalogInit=true)', () => {
199
+ let clock;
238
200
 
239
- sinon.assert.notCalled(services.initServiceCatalogs);
240
- });
201
+ beforeEach(() => {
202
+ services.webex.config = {
203
+ ...(services.webex.config || {}),
204
+ services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
205
+ };
206
+ // Reset ready flag: the outer beforeEach constructs services which
207
+ // runs initialize() once with no config (ungated path, flips ready to
208
+ // true). We reset here so tests that call initialize() a second time
209
+ // with the flag enabled observe the gated behavior from a clean state.
210
+ services.ready = false;
211
+ // The gated path schedules a real 15s setTimeout via _makeInitTimeout()
212
+ // that is not cleared when the init race resolves. Fake only setTimeout/
213
+ // clearTimeout (leaving setImmediate real so waitForAsync still works)
214
+ // so restoring in afterEach clears the pending timer and Jest can exit.
215
+ clock = sinon.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
216
+ });
241
217
 
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 = {};
218
+ afterEach(() => {
219
+ clock.restore();
220
+ });
251
221
 
252
- services.initialize();
222
+ it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
223
+ services.listenToOnce = sinon.stub();
224
+ services.initialize();
253
225
 
254
- // Get the catalog after initialize creates it
255
- const testCatalog = services._getCatalog();
256
- testCatalog.status.postauth.ready = false;
226
+ assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
227
+ });
257
228
 
258
- // call the onLoaded callback (preauth path)
259
- services.listenToOnce.getCall(1).args[2]();
260
- await waitForAsync();
229
+ it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
230
+ services.listenToOnce = sinon.stub();
231
+ services.initialize();
261
232
 
262
- // Now set canAuthorize to true to simulate auth completing
263
- services.webex.canAuthorize = true;
233
+ // Fire change:config to trigger the mode-specific listener registration.
234
+ services.listenToOnce.getCall(0).args[2]();
264
235
 
265
- // call the change:canAuthorize callback (should be call 2)
266
- services.listenToOnce.getCall(2).args[2]();
267
- await waitForAsync();
236
+ // Call 0 is 'change:config'; call 1 is the catalog init listener.
237
+ const [, event] = services.listenToOnce.getCall(1).args;
238
+ assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
239
+ });
268
240
 
269
- sinon.assert.calledWith(
270
- services.logger.error,
271
- 'services: failed to init service catalogs after auth, auth failed'
272
- );
273
- });
241
+ it('flips services.ready=true after init succeeds with credentials', async () => {
242
+ services.listenToOnce = sinon.stub();
243
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
244
+ services.webex.credentials = {
245
+ supertoken: {access_token: 'token'},
246
+ };
247
+
248
+ services.initialize();
249
+ services.listenToOnce.getCall(0).args[2]();
250
+ services.listenToOnce.getCall(1).args[2]();
251
+ await waitForAsync();
252
+
253
+ assert.isTrue(services.ready);
254
+ });
255
+
256
+ it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
257
+ services.listenToOnce = sinon.stub();
258
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
259
+
260
+ services.initialize();
261
+ services.listenToOnce.getCall(0).args[2]();
262
+ services.listenToOnce.getCall(1).args[2]();
263
+ await waitForAsync();
264
+
265
+ assert.isTrue(services.ready);
266
+ });
267
+
268
+ it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
269
+ services.listenToOnce = sinon.stub();
270
+ services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
271
+ services.webex.credentials = {
272
+ supertoken: {access_token: 'token'},
273
+ };
274
+ services.logger.error = sinon.stub();
275
+
276
+ services.initialize();
277
+ services.listenToOnce.getCall(0).args[2]();
278
+ services.listenToOnce.getCall(1).args[2]();
279
+ await waitForAsync();
280
+
281
+ assert.isTrue(services.initFailed);
282
+ assert.isTrue(services.ready);
283
+ });
284
+
285
+ it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
286
+ services.listenToOnce = sinon.stub();
287
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
288
+ services.logger.error = sinon.stub();
289
+
290
+ services.initialize();
291
+ services.listenToOnce.getCall(0).args[2]();
292
+ services.listenToOnce.getCall(1).args[2]();
293
+ await waitForAsync();
294
+
295
+ assert.isTrue(services.initFailed);
296
+ assert.isTrue(services.ready);
297
+ });
298
+
299
+ it('flips services.ready=true when init times out', async () => {
300
+ services.listenToOnce = sinon.stub();
301
+ // Never resolves - the timeout must win the race.
302
+ services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
303
+ services.webex.credentials = {
304
+ supertoken: {access_token: 'token'},
305
+ };
306
+ services.logger.error = sinon.stub();
307
+
308
+ services.initialize();
309
+ services.listenToOnce.getCall(0).args[2]();
310
+ services.listenToOnce.getCall(1).args[2]();
311
+
312
+ // Advance past the 15s init timeout and flush microtasks so the
313
+ // rejected timeout can propagate through .catch and .finally into
314
+ // _finalizeReady().
315
+ await clock.tickAsync(15_001);
316
+
317
+ assert.isTrue(services.initFailed);
318
+ assert.isTrue(services.ready);
319
+ sinon.assert.calledWithMatch(
320
+ services.logger.error,
321
+ /services: init timed out after 15000ms/
322
+ );
323
+ });
324
+
325
+ it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
326
+ services.listenToOnce = sinon.stub();
327
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
328
+
329
+ let onChangeIsRefreshing;
330
+ services.webex.credentials = {
331
+ supertoken: {access_token: 'token'},
332
+ isRefreshing: true,
333
+ once: sinon.stub().callsFake((event, cb) => {
334
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
335
+ }),
336
+ };
337
+
338
+ services.initialize();
339
+ services.listenToOnce.getCall(0).args[2]();
340
+ services.listenToOnce.getCall(1).args[2]();
341
+ await waitForAsync();
342
+
343
+ assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
344
+ assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
345
+ sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
346
+
347
+ // Simulate refresh completing.
348
+ onChangeIsRefreshing();
349
+ await waitForAsync();
350
+
351
+ assert.isTrue(services.ready);
352
+ });
353
+
354
+ it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
355
+ services.listenToOnce = sinon.stub();
356
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
357
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
358
+
359
+ services.initialize();
360
+ // initialize() creates a new catalog and replaces the WeakMap entry.
361
+ const currentCatalog = services._getCatalog();
362
+ // Fire the 'loaded' listener (index 1; index 0 was 'change:config').
363
+ services.listenToOnce.getCall(0).args[2]();
364
+ services.listenToOnce.getCall(1).args[2]();
365
+ await waitForAsync();
366
+
367
+ const freshLoginCall = services.listenToOnce
368
+ .getCalls()
369
+ .find((call) => call.args[1] === 'change:canAuthorize');
370
+ assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
371
+
372
+ // Simulate OAuth completing.
373
+ services.webex.canAuthorize = true;
374
+ currentCatalog.status.postauth.ready = false;
375
+ freshLoginCall.args[2]();
376
+
377
+ assert.isTrue(
378
+ services.initServiceCatalogs.called,
379
+ 'expected postauth catalog init after canAuthorize flips'
380
+ );
381
+
382
+ // `catalog.isReady` is now set inside initServiceCatalogs (after the
383
+ // postauth updateServices) rather than by this listener, so it is not
384
+ // asserted here where initServiceCatalogs is stubbed.
385
+ await waitForAsync();
386
+ });
387
+
388
+ it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
389
+ services.listenToOnce = sinon.stub();
390
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
391
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
392
+
393
+ services.initialize();
394
+ // initialize() creates a new catalog; grab the current one.
395
+ const currentCatalog = services._getCatalog();
396
+ services.listenToOnce.getCall(0).args[2]();
397
+ services.listenToOnce.getCall(1).args[2]();
398
+ await waitForAsync();
399
+
400
+ const freshLoginCall = services.listenToOnce
401
+ .getCalls()
402
+ .find((call) => call.args[1] === 'change:canAuthorize');
403
+
404
+ services.webex.canAuthorize = true;
405
+ currentCatalog.status.postauth.ready = true; // already collected
406
+ freshLoginCall.args[2]();
407
+
408
+ assert.isFalse(
409
+ services.initServiceCatalogs.called,
410
+ 'must not re-fetch postauth catalog if it is already ready'
411
+ );
412
+ });
413
+
414
+ it('finalizes ready when cache is warm (skips network fetch)', async () => {
415
+ services.listenToOnce = sinon.stub();
416
+ services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
417
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
418
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
419
+
420
+ services.initialize();
421
+ services.listenToOnce.getCall(0).args[2]();
422
+ services.listenToOnce.getCall(1).args[2]();
423
+ await waitForAsync();
424
+ await waitForAsync();
425
+
426
+ assert.isTrue(services.ready);
427
+ assert.isFalse(
428
+ services.initServiceCatalogs.called,
429
+ 'must skip network fetch when cache is warm'
430
+ );
431
+ assert.isFalse(
432
+ services.collectPreauthCatalog.called,
433
+ 'must skip preauth fetch when cache is warm'
434
+ );
435
+ });
436
+ });
437
+
438
+ describe('#initialize (waitForCatalogInit=true, skipPreauthCatalogOnUnauthenticated=true)', () => {
439
+ beforeEach(() => {
440
+ services.webex.config = {
441
+ ...(services.webex.config || {}),
442
+ services: {
443
+ waitForCatalogInit: true,
444
+ catalogInitTimeout: 15000,
445
+ skipPreauthCatalogOnUnauthenticated: true,
446
+ },
447
+ };
448
+ services.ready = false;
449
+ });
450
+
451
+ it('skips preauth catalog collection but still finalizes services.ready when unauthenticated', async () => {
452
+ services.listenToOnce = sinon.stub();
453
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
454
+ services.logger.info = sinon.stub();
455
+
456
+ services.initialize();
457
+ services.listenToOnce.getCall(0).args[2]();
458
+ services.listenToOnce.getCall(1).args[2]();
459
+ await waitForAsync();
460
+
461
+ assert.isFalse(
462
+ services.collectPreauthCatalog.called,
463
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
464
+ );
465
+ assert.isTrue(services.ready, 'must still finalize ready so webex.ready is not stalled');
466
+ sinon.assert.calledWith(
467
+ services.logger.info,
468
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
469
+ );
470
+ });
471
+
472
+ it('still registers a change:canAuthorize listener when skipping preauth (fresh-login path)', async () => {
473
+ services.listenToOnce = sinon.stub();
474
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
475
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
476
+
477
+ services.initialize();
478
+ const currentCatalog = services._getCatalog();
479
+ services.listenToOnce.getCall(0).args[2]();
480
+ services.listenToOnce.getCall(1).args[2]();
481
+ await waitForAsync();
482
+
483
+ const freshLoginCall = services.listenToOnce
484
+ .getCalls()
485
+ .find((call) => call.args[1] === 'change:canAuthorize');
486
+ assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
487
+
488
+ services.webex.canAuthorize = true;
489
+ currentCatalog.status.postauth.ready = false;
490
+ freshLoginCall.args[2]();
491
+
492
+ assert.isTrue(
493
+ services.initServiceCatalogs.called,
494
+ 'expected postauth catalog init after canAuthorize flips'
495
+ );
496
+ await waitForAsync();
497
+ });
498
+
499
+ it('does not schedule an init timeout when skipping preauth (no stray timer/rejection)', async () => {
500
+ const clock = sinon.useFakeTimers();
501
+ services.listenToOnce = sinon.stub();
502
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
503
+ services.logger.error = sinon.stub();
504
+
505
+ services.initialize();
506
+ services.listenToOnce.getCall(0).args[2]();
507
+ services.listenToOnce.getCall(1).args[2]();
508
+
509
+ // Advance well past the configured init timeout; since no timeout was
510
+ // scheduled in skip mode, no timeout error should ever be logged.
511
+ await clock.tickAsync(30_000);
512
+ clock.restore();
513
+
514
+ sinon.assert.neverCalledWithMatch(services.logger.error, /services: init timed out/);
515
+ });
516
+ });
517
+
518
+ describe('#_finalizeReady', () => {
519
+ beforeEach(() => {
520
+ // Outer beforeEach construction already ran initialize() (ungated,
521
+ // sets ready=true). Reset so _finalizeReady's flip is observable.
522
+ services.ready = false;
523
+ });
524
+
525
+ it('sets ready=true immediately when credentials are not refreshing', async () => {
526
+ services.webex.credentials = {isRefreshing: false};
527
+ assert.isFalse(services.ready);
528
+
529
+ await services._finalizeReady();
530
+
531
+ assert.isTrue(services.ready);
532
+ });
533
+
534
+ it('sets ready=true when credentials are missing', async () => {
535
+ services.webex.credentials = undefined;
536
+
537
+ await services._finalizeReady();
538
+
539
+ assert.isTrue(services.ready);
540
+ });
541
+
542
+ it('awaits change:isRefreshing before setting ready=true', async () => {
543
+ let onChangeIsRefreshing;
544
+ services.webex.credentials = {
545
+ isRefreshing: true,
546
+ once: sinon.stub().callsFake((event, cb) => {
547
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
548
+ }),
549
+ };
550
+
551
+ const settled = services._finalizeReady();
552
+ // Let the async body reach its await point.
553
+ await waitForAsync();
554
+
555
+ assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
556
+ assert.isFunction(onChangeIsRefreshing);
557
+
558
+ onChangeIsRefreshing();
559
+ await settled;
560
+
561
+ assert.isTrue(services.ready);
274
562
  });
275
563
  });
276
564
 
@@ -309,7 +597,7 @@ describe('webex-core', () => {
309
597
 
310
598
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
311
599
  return Promise.resolve();
312
- });
600
+ })
313
601
 
314
602
  services.updateServices = sinon.stub().callsFake(() => {
315
603
  return Promise.reject(error);
@@ -404,7 +692,7 @@ describe('webex-core', () => {
404
692
  discovery: {
405
693
  sqdiscovery: 'https://test.ciscospark.com/v1/region',
406
694
  },
407
- },
695
+ }
408
696
  };
409
697
  });
410
698
 
@@ -424,8 +712,8 @@ describe('webex-core', () => {
424
712
  assert.calledWith(webex.request, {
425
713
  uri: 'https://test.ciscospark.com/v1/region',
426
714
  addAuthHeader: false,
427
- headers: {'spark-user-agent': null},
428
- timeout: 5000,
715
+ headers: { 'spark-user-agent': null },
716
+ timeout: 5000
429
717
  });
430
718
  });
431
719
  });
@@ -482,6 +770,7 @@ describe('webex-core', () => {
482
770
  });
483
771
 
484
772
  describe('#_fetchNewServiceHostmap()', () => {
773
+
485
774
  beforeEach(() => {
486
775
  sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
487
776
  });
@@ -495,20 +784,17 @@ describe('webex-core', () => {
495
784
 
496
785
  sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
497
786
  sinon.stub(services, 'request').resolves({});
498
-
787
+
499
788
  const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
500
789
 
501
790
  assert.calledOnceWithExactly(services.request, {
502
791
  method: 'GET',
503
792
  service: 'u2c',
504
793
  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'
794
+ qs: {format: 'hostmap'}
795
+ }
511
796
  );
797
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
512
798
  });
513
799
 
514
800
  it('checks service request rejects', async () => {
@@ -516,7 +802,7 @@ describe('webex-core', () => {
516
802
 
517
803
  sinon.spy(services, '_formatReceivedHostmap');
518
804
  sinon.stub(services, 'request').rejects(error);
519
-
805
+
520
806
  const promise = services._fetchNewServiceHostmap({from: 'limited'});
521
807
  const rejectedValue = await assert.isRejected(promise);
522
808
 
@@ -528,13 +814,10 @@ describe('webex-core', () => {
528
814
  method: 'GET',
529
815
  service: 'u2c',
530
816
  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'
817
+ qs: {format: 'hostmap'}
818
+ }
537
819
  );
820
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
538
821
  });
539
822
  });
540
823
 
@@ -565,6 +848,7 @@ describe('webex-core', () => {
565
848
  });
566
849
 
567
850
  it('returns the original uri if the hostmap has no hosts for the host', () => {
851
+
568
852
  services._hostCatalog = {
569
853
  'example.com': [],
570
854
  };
@@ -728,7 +1012,8 @@ describe('webex-core', () => {
728
1012
  id: '0:0:0:different-e-x',
729
1013
  },
730
1014
  ],
731
- 'example-f.com': [],
1015
+ 'example-f.com': [
1016
+ ],
732
1017
  },
733
1018
  format: 'hostmap',
734
1019
  };
@@ -936,7 +1221,7 @@ describe('webex-core', () => {
936
1221
  defaultUrl: 'https://example-g.com/api/v1',
937
1222
  hosts: [],
938
1223
  name: 'example-g',
939
- },
1224
+ }
940
1225
  ]);
941
1226
  });
942
1227
 
@@ -990,59 +1275,34 @@ describe('webex-core', () => {
990
1275
  assert.equal(webex.config.credentials.authorizeUrl, authUrl);
991
1276
  });
992
1277
  });
993
-
1278
+
994
1279
  describe('#getMobiusClusters', () => {
995
1280
  it('returns unique mobius host entries from hostCatalog', () => {
996
1281
  // Arrange: two hostCatalog keys, with duplicate mobius host across keys
997
1282
  services._hostCatalog = {
998
1283
  '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
- ],
1284
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
1285
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
1286
+ ],
1012
1287
 
1013
1288
  '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': [
1289
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
1290
+ ],
1291
+ 'wdm-a.webex.com' : [
1022
1292
  {host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
1023
- ],
1293
+ ]
1024
1294
  };
1025
-
1295
+
1026
1296
  // Act
1027
1297
  const clusters = services.getMobiusClusters();
1028
-
1298
+
1029
1299
  // Assert
1030
1300
  // deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
1031
1301
  assert.deepEqual(
1032
1302
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
1033
1303
  [
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
- },
1304
+ {host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
1305
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
1046
1306
  ]
1047
1307
  );
1048
1308
  });
@@ -1051,31 +1311,31 @@ describe('webex-core', () => {
1051
1311
  describe('#isValidHost', () => {
1052
1312
  beforeEach(() => {
1053
1313
  // 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
- };
1314
+ services._hostCatalog = {
1315
+ "audit-ci-m.wbx2.com": [
1316
+ {
1317
+ "host": "audit-ci-m.wbx2.com",
1318
+ "ttl": -1,
1319
+ "priority": 5,
1320
+ "id": "urn:IDENTITY:PA61:adminAudit"
1321
+ },
1322
+ {
1323
+ "host": "audit-ci-m.wbx2.com",
1324
+ "ttl": -1,
1325
+ "priority": 5,
1326
+ "id": "urn:IDENTITY:PA61:adminAuditV2"
1327
+ }
1328
+ ],
1329
+ "mercury-connection-partition0-r.wbx2.com": [
1330
+ {
1331
+ "host": "mercury-connection-partition0-r.wbx2.com",
1332
+ "ttl": -1,
1333
+ "priority": 5,
1334
+ "id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
1335
+ }
1336
+ ],
1337
+ "empty.com": []
1338
+ };
1079
1339
  });
1080
1340
  afterAll(() => {
1081
1341
  // Clean up the mock host catalog
@@ -1100,13 +1360,64 @@ describe('webex-core', () => {
1100
1360
  });
1101
1361
  });
1102
1362
 
1363
+ describe('#isIntegrationEnvironment', () => {
1364
+ it('returns true when u2c URL contains "intb"', () => {
1365
+ services.webex.config = {
1366
+ services: {
1367
+ discovery: {
1368
+ u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
1369
+ },
1370
+ },
1371
+ };
1372
+ assert.isTrue(services.isIntegrationEnvironment());
1373
+ });
1374
+
1375
+ it('returns false when u2c URL does not contain "intb" (production)', () => {
1376
+ services.webex.config = {
1377
+ services: {
1378
+ discovery: {
1379
+ u2c: 'https://u2c.wbx2.com/u2c/api/v1',
1380
+ },
1381
+ },
1382
+ };
1383
+ assert.isFalse(services.isIntegrationEnvironment());
1384
+ });
1385
+
1386
+ it('returns false when u2c URL is for FedRAMP', () => {
1387
+ services.webex.config = {
1388
+ services: {
1389
+ discovery: {
1390
+ u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
1391
+ },
1392
+ },
1393
+ };
1394
+ assert.isFalse(services.isIntegrationEnvironment());
1395
+ });
1396
+
1397
+ it('returns false when u2c URL is undefined', () => {
1398
+ services.webex.config = {
1399
+ services: {
1400
+ discovery: {
1401
+ u2c: undefined,
1402
+ },
1403
+ },
1404
+ };
1405
+ assert.isFalse(services.isIntegrationEnvironment());
1406
+ });
1407
+
1408
+ it('returns false when config is not available', () => {
1409
+ services.webex.config = undefined;
1410
+ assert.isFalse(services.isIntegrationEnvironment());
1411
+ });
1412
+ });
1413
+
1103
1414
  describe('U2C catalog cache behavior', () => {
1104
1415
  let webex;
1105
1416
  let services;
1106
1417
  let catalog;
1107
1418
  let localStorageBackup;
1108
1419
  let windowBackup;
1109
-
1420
+
1110
1421
  const makeLocalStorageShim = () => {
1111
1422
  const store = new Map();
1112
1423
  return {
@@ -1116,16 +1427,13 @@ describe('webex-core', () => {
1116
1427
  _store: store,
1117
1428
  };
1118
1429
  };
1119
-
1430
+
1120
1431
  beforeEach(() => {
1121
1432
  // Build a fresh webex instance
1122
- webex = new MockWebex({
1123
- children: {services: Services},
1124
- config: {credentials: {federation: true}},
1125
- });
1433
+ webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
1126
1434
  services = webex.internal.services;
1127
1435
  catalog = services._getCatalog();
1128
-
1436
+
1129
1437
  // enable U2C caching feature flag in tests that rely on localStorage writes/reads
1130
1438
  services.webex.config = services.webex.config || {};
1131
1439
  services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
@@ -1137,15 +1445,13 @@ describe('webex-core', () => {
1137
1445
  global.window.localStorage = makeLocalStorageShim();
1138
1446
  // Ensure code under test uses our shim via util method
1139
1447
  sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
1140
-
1448
+
1141
1449
  // Stub the formatter so we don't need a full hostmap payload in tests
1142
- sinon
1143
- .stub(services, '_formatReceivedHostmap')
1144
- .callsFake(() => [
1145
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1146
- ]);
1450
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1451
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1452
+ ]);
1147
1453
  });
1148
-
1454
+
1149
1455
  afterEach(() => {
1150
1456
  global.window.localStorage = localStorageBackup || undefined;
1151
1457
  if (!windowBackup) {
@@ -1158,7 +1464,7 @@ describe('webex-core', () => {
1158
1464
  services._getLocalStorageSafe.restore();
1159
1465
  }
1160
1466
  });
1161
-
1467
+
1162
1468
  it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
1163
1469
  // Arrange: authenticated credentials and spies
1164
1470
  services.webex.credentials = {
@@ -1170,20 +1476,10 @@ describe('webex-core', () => {
1170
1476
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1171
1477
  const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
1172
1478
  // Make fetch return a hostmap object and allow formatter to reduce it
1173
- sinon
1174
- .stub(services, 'request')
1175
- .resolves({
1176
- body: {
1177
- services: [],
1178
- activeServices: {},
1179
- timestamp: Date.now().toString(),
1180
- orgId: 'urn:EXAMPLE:org',
1181
- format: 'U2CV2',
1182
- },
1183
- });
1184
- // Cause loaded callback to run immediately
1479
+ sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
1480
+ // Cause ready callback to run immediately
1185
1481
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1186
- if (event === 'loaded') cb();
1482
+ if (event === 'change:config' || event === 'ready') cb();
1187
1483
  });
1188
1484
 
1189
1485
  // Act
@@ -1223,9 +1519,9 @@ describe('webex-core', () => {
1223
1519
 
1224
1520
  const initSpy = sinon.spy(services, 'initServiceCatalogs');
1225
1521
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1226
- // Cause loaded callback to run immediately
1522
+ // Cause ready callback to run immediately
1227
1523
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1228
- if (event === 'loaded') cb();
1524
+ if (event === 'change:config' || event === 'ready') cb();
1229
1525
  });
1230
1526
 
1231
1527
  // Act
@@ -1233,18 +1529,9 @@ describe('webex-core', () => {
1233
1529
  await waitForAsync();
1234
1530
 
1235
1531
  // Assert: ready path found cache and skipped initServiceCatalogs
1236
- assert.isFalse(
1237
- initSpy.called,
1238
- 'expected initServiceCatalogs to be skipped with cache present'
1239
- );
1240
- assert.isTrue(
1241
- services._getCatalog().status.preauth.ready,
1242
- 'preauth should be ready from cache'
1243
- );
1244
- assert.isTrue(
1245
- services._getCatalog().status.postauth.ready,
1246
- 'postauth should be ready from cache'
1247
- );
1532
+ assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
1533
+ assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
1534
+ assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
1248
1535
  assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
1249
1536
 
1250
1537
  // Cleanup
@@ -1260,29 +1547,26 @@ describe('webex-core', () => {
1260
1547
  preauth: {serviceLinks: {}, hostCatalog: {}},
1261
1548
  postauth: {serviceLinks: {}, hostCatalog: {}},
1262
1549
  };
1263
-
1550
+
1264
1551
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
1265
-
1552
+
1266
1553
  const warmed = await services._loadCatalogFromCache();
1267
-
1554
+
1268
1555
  assert.isFalse(warmed, 'stale cache must not warm');
1269
- assert.isNull(
1270
- window.localStorage.getItem(CATALOG_CACHE_KEY_V1),
1271
- 'expired cache must be cleared'
1272
- );
1556
+ assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
1273
1557
  assert.isFalse(catalog.status.preauth.ready);
1274
1558
  assert.isFalse(catalog.status.postauth.ready);
1275
1559
  });
1276
-
1560
+
1277
1561
  it('clearCatalogCache() removes the cached entry', async () => {
1278
1562
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1279
1563
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
1280
-
1564
+
1281
1565
  await services.clearCatalogCache();
1282
-
1566
+
1283
1567
  assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
1284
1568
  });
1285
-
1569
+
1286
1570
  it('still fetches when forceRefresh=true even if ready', async () => {
1287
1571
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1288
1572
  window.localStorage.setItem(
@@ -1294,24 +1578,20 @@ describe('webex-core', () => {
1294
1578
  postauth: {serviceLinks: {}, hostCatalog: {}},
1295
1579
  })
1296
1580
  );
1297
-
1581
+
1298
1582
  // warm from cache
1299
1583
  const warmed = await services._loadCatalogFromCache();
1300
1584
  assert.isTrue(warmed);
1301
1585
  assert.isTrue(catalog.status.preauth.ready);
1302
1586
  assert.isTrue(catalog.status.postauth.ready);
1303
-
1587
+
1304
1588
  const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
1305
-
1589
+
1306
1590
  // with forceRefresh we should fetch despite ready=true
1307
- await services.updateServices({
1308
- from: 'limited',
1309
- query: {orgId: 'urn:EXAMPLE:org'},
1310
- forceRefresh: true,
1311
- });
1591
+ await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
1312
1592
  // pass an empty query to avoid spreading undefined in qs construction
1313
1593
  await services.updateServices({forceRefresh: true});
1314
-
1594
+
1315
1595
  assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
1316
1596
  fetchSpy.restore();
1317
1597
  });
@@ -1372,11 +1652,9 @@ describe('webex-core', () => {
1372
1652
  );
1373
1653
  // formatter returns at least one entry to mark ready
1374
1654
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1375
- sinon
1376
- .stub(services, '_formatReceivedHostmap')
1377
- .callsFake(() => [
1378
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1379
- ]);
1655
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1656
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1657
+ ]);
1380
1658
 
1381
1659
  const warmed = await services._loadCatalogFromCache();
1382
1660
  assert.isTrue(warmed);
@@ -1398,11 +1676,9 @@ describe('webex-core', () => {
1398
1676
  })
1399
1677
  );
1400
1678
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1401
- sinon
1402
- .stub(services, '_formatReceivedHostmap')
1403
- .callsFake(() => [
1404
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1405
- ]);
1679
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1680
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1681
+ ]);
1406
1682
 
1407
1683
  const warmed = await services._loadCatalogFromCache();
1408
1684
  // function returns true if overall cache path succeeded; we only verify group readiness
@@ -1429,17 +1705,12 @@ describe('webex-core', () => {
1429
1705
  })
1430
1706
  );
1431
1707
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1432
- sinon
1433
- .stub(services, '_formatReceivedHostmap')
1434
- .callsFake(() => [
1435
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1436
- ]);
1708
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1709
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1710
+ ]);
1437
1711
 
1438
1712
  await services._loadCatalogFromCache();
1439
- assert.isFalse(
1440
- catalog.status.preauth.ready,
1441
- 'preauth should not warm on selection mismatch'
1442
- );
1713
+ assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
1443
1714
  });
1444
1715
 
1445
1716
  it('skips warming when environment fingerprint mismatches', async () => {
@@ -1450,10 +1721,7 @@ describe('webex-core', () => {
1450
1721
  JSON.stringify({
1451
1722
  cachedAt: Date.now(),
1452
1723
  env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
1453
- preauth: {
1454
- hostMap: {serviceLinks: {}, hostCatalog: {}},
1455
- meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
1456
- },
1724
+ preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
1457
1725
  })
1458
1726
  );
1459
1727
  // current env
@@ -1468,6 +1736,7 @@ describe('webex-core', () => {
1468
1736
  assert.isFalse(catalog.status.postauth.ready);
1469
1737
  });
1470
1738
  });
1739
+
1471
1740
  });
1472
1741
  });
1473
1742
  /* eslint-enable no-underscore-dangle */