@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-llmrefactor.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 +15 -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 +208 -51
  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 +207 -46
  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 +17 -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 +174 -37
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +173 -33
  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 +49 -24
  40. package/test/integration/spec/services-v2/services-v2.js +49 -24
  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 +458 -322
  46. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  47. package/test/unit/spec/services-v2/services-v2.ts +403 -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,334 @@ 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, catalogInitTimeout: 15000},
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
+ // `catalog.isReady` is now set inside initServiceCatalogs (after the
330
+ // postauth updateServices) rather than by this listener, so it is not
331
+ // asserted here where initServiceCatalogs is stubbed.
332
+ await waitForAsync();
333
+ });
334
+
335
+ it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
336
+ services.listenToOnce = sinon.stub();
337
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
338
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
339
+
340
+ services.initialize();
341
+ // initialize() creates a new catalog; grab the current one.
342
+ const currentCatalog = services._getCatalog();
343
+ services.listenToOnce.getCall(0).args[2]();
344
+ services.listenToOnce.getCall(1).args[2]();
345
+ await waitForAsync();
346
+
347
+ const freshLoginCall = services.listenToOnce
348
+ .getCalls()
349
+ .find((call) => call.args[1] === 'change:canAuthorize');
350
+
351
+ services.webex.canAuthorize = true;
352
+ currentCatalog.status.postauth.ready = true; // already collected
353
+ freshLoginCall.args[2]();
354
+
355
+ assert.isFalse(
356
+ services.initServiceCatalogs.called,
357
+ 'must not re-fetch postauth catalog if it is already ready'
358
+ );
359
+ });
360
+
361
+ it('finalizes ready when cache is warm (skips network fetch)', async () => {
362
+ services.listenToOnce = sinon.stub();
363
+ services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
364
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
365
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
366
+
367
+ services.initialize();
368
+ services.listenToOnce.getCall(0).args[2]();
369
+ services.listenToOnce.getCall(1).args[2]();
370
+ await waitForAsync();
371
+ await waitForAsync();
372
+
373
+ assert.isTrue(services.ready);
374
+ assert.isFalse(
375
+ services.initServiceCatalogs.called,
376
+ 'must skip network fetch when cache is warm'
377
+ );
378
+ assert.isFalse(
379
+ services.collectPreauthCatalog.called,
380
+ 'must skip preauth fetch when cache is warm'
381
+ );
382
+ });
383
+ });
384
+
385
+ describe('#_finalizeReady', () => {
386
+ beforeEach(() => {
387
+ // Outer beforeEach construction already ran initialize() (ungated,
388
+ // sets ready=true). Reset so _finalizeReady's flip is observable.
389
+ services.ready = false;
390
+ });
391
+
392
+ it('sets ready=true immediately when credentials are not refreshing', async () => {
393
+ services.webex.credentials = {isRefreshing: false};
394
+ assert.isFalse(services.ready);
395
+
396
+ await services._finalizeReady();
397
+
398
+ assert.isTrue(services.ready);
399
+ });
400
+
401
+ it('sets ready=true when credentials are missing', async () => {
402
+ services.webex.credentials = undefined;
403
+
404
+ await services._finalizeReady();
405
+
406
+ assert.isTrue(services.ready);
407
+ });
408
+
409
+ it('awaits change:isRefreshing before setting ready=true', async () => {
410
+ let onChangeIsRefreshing;
411
+ services.webex.credentials = {
412
+ isRefreshing: true,
413
+ once: sinon.stub().callsFake((event, cb) => {
414
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
415
+ }),
416
+ };
417
+
418
+ const settled = services._finalizeReady();
419
+ // Let the async body reach its await point.
420
+ await waitForAsync();
421
+
422
+ assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
423
+ assert.isFunction(onChangeIsRefreshing);
424
+
425
+ onChangeIsRefreshing();
426
+ await settled;
427
+
428
+ assert.isTrue(services.ready);
274
429
  });
275
430
  });
276
431
 
@@ -309,7 +464,7 @@ describe('webex-core', () => {
309
464
 
310
465
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
311
466
  return Promise.resolve();
312
- });
467
+ })
313
468
 
314
469
  services.updateServices = sinon.stub().callsFake(() => {
315
470
  return Promise.reject(error);
@@ -404,7 +559,7 @@ describe('webex-core', () => {
404
559
  discovery: {
405
560
  sqdiscovery: 'https://test.ciscospark.com/v1/region',
406
561
  },
407
- },
562
+ }
408
563
  };
409
564
  });
410
565
 
@@ -424,8 +579,8 @@ describe('webex-core', () => {
424
579
  assert.calledWith(webex.request, {
425
580
  uri: 'https://test.ciscospark.com/v1/region',
426
581
  addAuthHeader: false,
427
- headers: {'spark-user-agent': null},
428
- timeout: 5000,
582
+ headers: { 'spark-user-agent': null },
583
+ timeout: 5000
429
584
  });
430
585
  });
431
586
  });
@@ -482,6 +637,7 @@ describe('webex-core', () => {
482
637
  });
483
638
 
484
639
  describe('#_fetchNewServiceHostmap()', () => {
640
+
485
641
  beforeEach(() => {
486
642
  sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
487
643
  });
@@ -495,20 +651,17 @@ describe('webex-core', () => {
495
651
 
496
652
  sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
497
653
  sinon.stub(services, 'request').resolves({});
498
-
654
+
499
655
  const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
500
656
 
501
657
  assert.calledOnceWithExactly(services.request, {
502
658
  method: 'GET',
503
659
  service: 'u2c',
504
660
  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'
661
+ qs: {format: 'hostmap'}
662
+ }
511
663
  );
664
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
512
665
  });
513
666
 
514
667
  it('checks service request rejects', async () => {
@@ -516,7 +669,7 @@ describe('webex-core', () => {
516
669
 
517
670
  sinon.spy(services, '_formatReceivedHostmap');
518
671
  sinon.stub(services, 'request').rejects(error);
519
-
672
+
520
673
  const promise = services._fetchNewServiceHostmap({from: 'limited'});
521
674
  const rejectedValue = await assert.isRejected(promise);
522
675
 
@@ -528,13 +681,10 @@ describe('webex-core', () => {
528
681
  method: 'GET',
529
682
  service: 'u2c',
530
683
  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'
684
+ qs: {format: 'hostmap'}
685
+ }
537
686
  );
687
+ assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
538
688
  });
539
689
  });
540
690
 
@@ -565,6 +715,7 @@ describe('webex-core', () => {
565
715
  });
566
716
 
567
717
  it('returns the original uri if the hostmap has no hosts for the host', () => {
718
+
568
719
  services._hostCatalog = {
569
720
  'example.com': [],
570
721
  };
@@ -728,7 +879,8 @@ describe('webex-core', () => {
728
879
  id: '0:0:0:different-e-x',
729
880
  },
730
881
  ],
731
- 'example-f.com': [],
882
+ 'example-f.com': [
883
+ ],
732
884
  },
733
885
  format: 'hostmap',
734
886
  };
@@ -936,7 +1088,7 @@ describe('webex-core', () => {
936
1088
  defaultUrl: 'https://example-g.com/api/v1',
937
1089
  hosts: [],
938
1090
  name: 'example-g',
939
- },
1091
+ }
940
1092
  ]);
941
1093
  });
942
1094
 
@@ -990,59 +1142,34 @@ describe('webex-core', () => {
990
1142
  assert.equal(webex.config.credentials.authorizeUrl, authUrl);
991
1143
  });
992
1144
  });
993
-
1145
+
994
1146
  describe('#getMobiusClusters', () => {
995
1147
  it('returns unique mobius host entries from hostCatalog', () => {
996
1148
  // Arrange: two hostCatalog keys, with duplicate mobius host across keys
997
1149
  services._hostCatalog = {
998
1150
  '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
- ],
1151
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
1152
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
1153
+ ],
1012
1154
 
1013
1155
  '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': [
1156
+ {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
1157
+ ],
1158
+ 'wdm-a.webex.com' : [
1022
1159
  {host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
1023
- ],
1160
+ ]
1024
1161
  };
1025
-
1162
+
1026
1163
  // Act
1027
1164
  const clusters = services.getMobiusClusters();
1028
-
1165
+
1029
1166
  // Assert
1030
1167
  // deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
1031
1168
  assert.deepEqual(
1032
1169
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
1033
1170
  [
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
- },
1171
+ {host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
1172
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
1046
1173
  ]
1047
1174
  );
1048
1175
  });
@@ -1051,31 +1178,31 @@ describe('webex-core', () => {
1051
1178
  describe('#isValidHost', () => {
1052
1179
  beforeEach(() => {
1053
1180
  // 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
- };
1181
+ services._hostCatalog = {
1182
+ "audit-ci-m.wbx2.com": [
1183
+ {
1184
+ "host": "audit-ci-m.wbx2.com",
1185
+ "ttl": -1,
1186
+ "priority": 5,
1187
+ "id": "urn:IDENTITY:PA61:adminAudit"
1188
+ },
1189
+ {
1190
+ "host": "audit-ci-m.wbx2.com",
1191
+ "ttl": -1,
1192
+ "priority": 5,
1193
+ "id": "urn:IDENTITY:PA61:adminAuditV2"
1194
+ }
1195
+ ],
1196
+ "mercury-connection-partition0-r.wbx2.com": [
1197
+ {
1198
+ "host": "mercury-connection-partition0-r.wbx2.com",
1199
+ "ttl": -1,
1200
+ "priority": 5,
1201
+ "id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
1202
+ }
1203
+ ],
1204
+ "empty.com": []
1205
+ };
1079
1206
  });
1080
1207
  afterAll(() => {
1081
1208
  // Clean up the mock host catalog
@@ -1100,13 +1227,64 @@ describe('webex-core', () => {
1100
1227
  });
1101
1228
  });
1102
1229
 
1230
+ describe('#isIntegrationEnvironment', () => {
1231
+ it('returns true when u2c URL contains "intb"', () => {
1232
+ services.webex.config = {
1233
+ services: {
1234
+ discovery: {
1235
+ u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
1236
+ },
1237
+ },
1238
+ };
1239
+ assert.isTrue(services.isIntegrationEnvironment());
1240
+ });
1241
+
1242
+ it('returns false when u2c URL does not contain "intb" (production)', () => {
1243
+ services.webex.config = {
1244
+ services: {
1245
+ discovery: {
1246
+ u2c: 'https://u2c.wbx2.com/u2c/api/v1',
1247
+ },
1248
+ },
1249
+ };
1250
+ assert.isFalse(services.isIntegrationEnvironment());
1251
+ });
1252
+
1253
+ it('returns false when u2c URL is for FedRAMP', () => {
1254
+ services.webex.config = {
1255
+ services: {
1256
+ discovery: {
1257
+ u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
1258
+ },
1259
+ },
1260
+ };
1261
+ assert.isFalse(services.isIntegrationEnvironment());
1262
+ });
1263
+
1264
+ it('returns false when u2c URL is undefined', () => {
1265
+ services.webex.config = {
1266
+ services: {
1267
+ discovery: {
1268
+ u2c: undefined,
1269
+ },
1270
+ },
1271
+ };
1272
+ assert.isFalse(services.isIntegrationEnvironment());
1273
+ });
1274
+
1275
+ it('returns false when config is not available', () => {
1276
+ services.webex.config = undefined;
1277
+ assert.isFalse(services.isIntegrationEnvironment());
1278
+ });
1279
+ });
1280
+
1103
1281
  describe('U2C catalog cache behavior', () => {
1104
1282
  let webex;
1105
1283
  let services;
1106
1284
  let catalog;
1107
1285
  let localStorageBackup;
1108
1286
  let windowBackup;
1109
-
1287
+
1110
1288
  const makeLocalStorageShim = () => {
1111
1289
  const store = new Map();
1112
1290
  return {
@@ -1116,16 +1294,13 @@ describe('webex-core', () => {
1116
1294
  _store: store,
1117
1295
  };
1118
1296
  };
1119
-
1297
+
1120
1298
  beforeEach(() => {
1121
1299
  // Build a fresh webex instance
1122
- webex = new MockWebex({
1123
- children: {services: Services},
1124
- config: {credentials: {federation: true}},
1125
- });
1300
+ webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
1126
1301
  services = webex.internal.services;
1127
1302
  catalog = services._getCatalog();
1128
-
1303
+
1129
1304
  // enable U2C caching feature flag in tests that rely on localStorage writes/reads
1130
1305
  services.webex.config = services.webex.config || {};
1131
1306
  services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
@@ -1137,15 +1312,13 @@ describe('webex-core', () => {
1137
1312
  global.window.localStorage = makeLocalStorageShim();
1138
1313
  // Ensure code under test uses our shim via util method
1139
1314
  sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
1140
-
1315
+
1141
1316
  // 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
- ]);
1317
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1318
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1319
+ ]);
1147
1320
  });
1148
-
1321
+
1149
1322
  afterEach(() => {
1150
1323
  global.window.localStorage = localStorageBackup || undefined;
1151
1324
  if (!windowBackup) {
@@ -1158,7 +1331,7 @@ describe('webex-core', () => {
1158
1331
  services._getLocalStorageSafe.restore();
1159
1332
  }
1160
1333
  });
1161
-
1334
+
1162
1335
  it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
1163
1336
  // Arrange: authenticated credentials and spies
1164
1337
  services.webex.credentials = {
@@ -1170,20 +1343,10 @@ describe('webex-core', () => {
1170
1343
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1171
1344
  const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
1172
1345
  // 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
1346
+ sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
1347
+ // Cause ready callback to run immediately
1185
1348
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1186
- if (event === 'loaded') cb();
1349
+ if (event === 'change:config' || event === 'ready') cb();
1187
1350
  });
1188
1351
 
1189
1352
  // Act
@@ -1223,9 +1386,9 @@ describe('webex-core', () => {
1223
1386
 
1224
1387
  const initSpy = sinon.spy(services, 'initServiceCatalogs');
1225
1388
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1226
- // Cause loaded callback to run immediately
1389
+ // Cause ready callback to run immediately
1227
1390
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1228
- if (event === 'loaded') cb();
1391
+ if (event === 'change:config' || event === 'ready') cb();
1229
1392
  });
1230
1393
 
1231
1394
  // Act
@@ -1233,18 +1396,9 @@ describe('webex-core', () => {
1233
1396
  await waitForAsync();
1234
1397
 
1235
1398
  // 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
- );
1399
+ assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
1400
+ assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
1401
+ assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
1248
1402
  assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
1249
1403
 
1250
1404
  // Cleanup
@@ -1260,29 +1414,26 @@ describe('webex-core', () => {
1260
1414
  preauth: {serviceLinks: {}, hostCatalog: {}},
1261
1415
  postauth: {serviceLinks: {}, hostCatalog: {}},
1262
1416
  };
1263
-
1417
+
1264
1418
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
1265
-
1419
+
1266
1420
  const warmed = await services._loadCatalogFromCache();
1267
-
1421
+
1268
1422
  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
- );
1423
+ assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
1273
1424
  assert.isFalse(catalog.status.preauth.ready);
1274
1425
  assert.isFalse(catalog.status.postauth.ready);
1275
1426
  });
1276
-
1427
+
1277
1428
  it('clearCatalogCache() removes the cached entry', async () => {
1278
1429
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1279
1430
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
1280
-
1431
+
1281
1432
  await services.clearCatalogCache();
1282
-
1433
+
1283
1434
  assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
1284
1435
  });
1285
-
1436
+
1286
1437
  it('still fetches when forceRefresh=true even if ready', async () => {
1287
1438
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1288
1439
  window.localStorage.setItem(
@@ -1294,24 +1445,20 @@ describe('webex-core', () => {
1294
1445
  postauth: {serviceLinks: {}, hostCatalog: {}},
1295
1446
  })
1296
1447
  );
1297
-
1448
+
1298
1449
  // warm from cache
1299
1450
  const warmed = await services._loadCatalogFromCache();
1300
1451
  assert.isTrue(warmed);
1301
1452
  assert.isTrue(catalog.status.preauth.ready);
1302
1453
  assert.isTrue(catalog.status.postauth.ready);
1303
-
1454
+
1304
1455
  const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
1305
-
1456
+
1306
1457
  // 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
- });
1458
+ await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
1312
1459
  // pass an empty query to avoid spreading undefined in qs construction
1313
1460
  await services.updateServices({forceRefresh: true});
1314
-
1461
+
1315
1462
  assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
1316
1463
  fetchSpy.restore();
1317
1464
  });
@@ -1372,11 +1519,9 @@ describe('webex-core', () => {
1372
1519
  );
1373
1520
  // formatter returns at least one entry to mark ready
1374
1521
  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
- ]);
1522
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1523
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1524
+ ]);
1380
1525
 
1381
1526
  const warmed = await services._loadCatalogFromCache();
1382
1527
  assert.isTrue(warmed);
@@ -1398,11 +1543,9 @@ describe('webex-core', () => {
1398
1543
  })
1399
1544
  );
1400
1545
  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
- ]);
1546
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1547
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1548
+ ]);
1406
1549
 
1407
1550
  const warmed = await services._loadCatalogFromCache();
1408
1551
  // function returns true if overall cache path succeeded; we only verify group readiness
@@ -1429,17 +1572,12 @@ describe('webex-core', () => {
1429
1572
  })
1430
1573
  );
1431
1574
  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
- ]);
1575
+ sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1576
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1577
+ ]);
1437
1578
 
1438
1579
  await services._loadCatalogFromCache();
1439
- assert.isFalse(
1440
- catalog.status.preauth.ready,
1441
- 'preauth should not warm on selection mismatch'
1442
- );
1580
+ assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
1443
1581
  });
1444
1582
 
1445
1583
  it('skips warming when environment fingerprint mismatches', async () => {
@@ -1450,10 +1588,7 @@ describe('webex-core', () => {
1450
1588
  JSON.stringify({
1451
1589
  cachedAt: Date.now(),
1452
1590
  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
- },
1591
+ preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
1457
1592
  })
1458
1593
  );
1459
1594
  // current env
@@ -1468,6 +1603,7 @@ describe('webex-core', () => {
1468
1603
  assert.isFalse(catalog.status.postauth.ready);
1469
1604
  });
1470
1605
  });
1606
+
1471
1607
  });
1472
1608
  });
1473
1609
  /* eslint-enable no-underscore-dangle */