@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
 
@@ -116,7 +71,7 @@ describe('webex-core', () => {
116
71
  {error: new Error('failed'), expectedMessage: 'failed'},
117
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}`
@@ -147,7 +100,7 @@ describe('webex-core', () => {
147
100
  {error: new Error('failed'), expectedMessage: 'failed'},
148
101
  {error: undefined, expectedMessage: undefined},
149
102
  ])(
150
- 'sets initFailed to true when initServiceCatalogs errors but still sets ready to true',
103
+ 'sets initFailed to true when initServiceCatalogs errors',
151
104
  async ({error, expectedMessage}) => {
152
105
  services.initServiceCatalogs = sinon.stub().callsFake(() => {
153
106
  return Promise.reject(error);
@@ -160,18 +113,16 @@ describe('webex-core', () => {
160
113
 
161
114
  services.listenToOnce = sinon.stub();
162
115
  services.logger.error = sinon.stub();
163
- services.trigger = sinon.stub();
164
116
 
165
117
  services.initialize();
166
118
 
167
- // call the onLoaded callback
119
+ // call the onReady callback
120
+ services.listenToOnce.getCall(0).args[2]();
168
121
  services.listenToOnce.getCall(1).args[2]();
169
122
 
170
123
  await waitForAsync();
171
124
 
172
125
  assert.isTrue(services.initFailed);
173
- assert.isTrue(services.ready);
174
- sinon.assert.calledWith(services.trigger, 'services:initialized');
175
126
  sinon.assert.calledWith(
176
127
  services.logger.error,
177
128
  `services: failed to init initial services when credentials available, ${expectedMessage}`
@@ -179,98 +130,295 @@ describe('webex-core', () => {
179
130
  }
180
131
  );
181
132
 
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 = {};
133
+ it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
134
+ services.listenToOnce = sinon.stub();
135
+ services.initialize();
191
136
 
192
- services.initialize();
137
+ // Fire the change:config handler which decides gated-vs-ungated and, in
138
+ // ungated mode, flips services.ready to true.
139
+ services.listenToOnce.getCall(0).args[2]();
193
140
 
194
- // Get the catalog after initialize creates it
195
- const testCatalog = services._getCatalog();
196
- testCatalog.status.postauth.ready = false;
141
+ assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
142
+ });
197
143
 
198
- // call the onLoaded callback (preauth path)
199
- services.listenToOnce.getCall(1).args[2]();
200
- await waitForAsync();
144
+ it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
145
+ services.listenToOnce = sinon.stub();
146
+ services.initialize();
201
147
 
202
- // Now set canAuthorize to true to simulate auth completing
203
- services.webex.canAuthorize = true;
148
+ // Fire change:config to trigger the mode-specific listener registration.
149
+ services.listenToOnce.getCall(0).args[2]();
204
150
 
205
- // call the change:canAuthorize callback (should be call 2)
206
- services.listenToOnce.getCall(2).args[2]();
207
- await waitForAsync();
151
+ const [, event] = services.listenToOnce.getCall(1).args;
152
+ assert.equal(event, 'ready', 'default path listens on webex ready');
153
+ });
154
+ });
208
155
 
209
- sinon.assert.calledOnce(services.initServiceCatalogs);
210
- assert.isTrue(testCatalog.isReady);
211
- });
156
+ describe('#initialize (waitForCatalogInit=true)', () => {
157
+ beforeEach(() => {
158
+ services.webex.config = {
159
+ ...(services.webex.config || {}),
160
+ services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
161
+ };
162
+ // Reset ready flag: the outer beforeEach constructs services which
163
+ // runs initialize() once with no config (ungated path, flips ready to
164
+ // true). We reset here so tests that call initialize() a second time
165
+ // with the flag enabled observe the gated behavior from a clean state.
166
+ services.ready = false;
167
+ });
212
168
 
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 = {};
169
+ it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
170
+ services.listenToOnce = sinon.stub();
171
+ services.initialize();
221
172
 
222
- services.initialize();
173
+ assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
174
+ });
223
175
 
224
- // Get the catalog after initialize creates it
225
- const testCatalog = services._getCatalog();
176
+ it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
177
+ services.listenToOnce = sinon.stub();
178
+ services.initialize();
226
179
 
227
- // call the onLoaded callback (preauth path)
228
- services.listenToOnce.getCall(1).args[2]();
229
- await waitForAsync();
180
+ // Fire change:config to trigger the mode-specific listener registration.
181
+ services.listenToOnce.getCall(0).args[2]();
230
182
 
231
- // Set canAuthorize to true and postauth as ready BEFORE calling the callback
232
- services.webex.canAuthorize = true;
233
- testCatalog.status.postauth.ready = true;
183
+ const [, event] = services.listenToOnce.getCall(1).args;
184
+ assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
185
+ });
234
186
 
235
- // call the change:canAuthorize callback (should be call 2)
236
- services.listenToOnce.getCall(2).args[2]();
237
- await waitForAsync();
187
+ it('flips services.ready=true after init succeeds with credentials', async () => {
188
+ services.listenToOnce = sinon.stub();
189
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
190
+ services.webex.credentials = {
191
+ supertoken: {access_token: 'token'},
192
+ };
238
193
 
239
- sinon.assert.notCalled(services.initServiceCatalogs);
240
- });
194
+ services.initialize();
195
+ services.listenToOnce.getCall(0).args[2]();
196
+ services.listenToOnce.getCall(1).args[2]();
197
+ await waitForAsync();
241
198
 
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 = {};
199
+ assert.isTrue(services.ready);
200
+ });
251
201
 
252
- services.initialize();
202
+ it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
203
+ services.listenToOnce = sinon.stub();
204
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
253
205
 
254
- // Get the catalog after initialize creates it
255
- const testCatalog = services._getCatalog();
256
- testCatalog.status.postauth.ready = false;
206
+ services.initialize();
207
+ services.listenToOnce.getCall(0).args[2]();
208
+ services.listenToOnce.getCall(1).args[2]();
209
+ await waitForAsync();
257
210
 
258
- // call the onLoaded callback (preauth path)
259
- services.listenToOnce.getCall(1).args[2]();
260
- await waitForAsync();
211
+ assert.isTrue(services.ready);
212
+ });
261
213
 
262
- // Now set canAuthorize to true to simulate auth completing
263
- services.webex.canAuthorize = true;
214
+ it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
215
+ services.listenToOnce = sinon.stub();
216
+ services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
217
+ services.webex.credentials = {
218
+ supertoken: {access_token: 'token'},
219
+ };
220
+ services.logger.error = sinon.stub();
264
221
 
265
- // call the change:canAuthorize callback (should be call 2)
266
- services.listenToOnce.getCall(2).args[2]();
267
- await waitForAsync();
222
+ services.initialize();
223
+ services.listenToOnce.getCall(0).args[2]();
224
+ services.listenToOnce.getCall(1).args[2]();
225
+ await waitForAsync();
268
226
 
269
- sinon.assert.calledWith(
270
- services.logger.error,
271
- 'services: failed to init service catalogs after auth, auth failed'
272
- );
273
- });
227
+ assert.isTrue(services.initFailed);
228
+ assert.isTrue(services.ready);
229
+ });
230
+
231
+ it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
232
+ services.listenToOnce = sinon.stub();
233
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
234
+ services.logger.error = sinon.stub();
235
+
236
+ services.initialize();
237
+ services.listenToOnce.getCall(0).args[2]();
238
+ services.listenToOnce.getCall(1).args[2]();
239
+ await waitForAsync();
240
+
241
+ assert.isTrue(services.initFailed);
242
+ assert.isTrue(services.ready);
243
+ });
244
+
245
+ it('flips services.ready=true when init times out', async () => {
246
+ const clock = sinon.useFakeTimers();
247
+ services.listenToOnce = sinon.stub();
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
+ await clock.tickAsync(15_001);
259
+ clock.restore();
260
+
261
+ assert.isTrue(services.initFailed);
262
+ assert.isTrue(services.ready);
263
+ sinon.assert.calledWithMatch(
264
+ services.logger.error,
265
+ /services: init timed out after 15000ms/
266
+ );
267
+ });
268
+
269
+ it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
270
+ services.listenToOnce = sinon.stub();
271
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
272
+
273
+ let onChangeIsRefreshing: (() => void) | undefined;
274
+ services.webex.credentials = {
275
+ supertoken: {access_token: 'token'},
276
+ isRefreshing: true,
277
+ once: sinon.stub().callsFake((event: string, cb: () => void) => {
278
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
279
+ }),
280
+ };
281
+
282
+ services.initialize();
283
+ services.listenToOnce.getCall(0).args[2]();
284
+ services.listenToOnce.getCall(1).args[2]();
285
+ await waitForAsync();
286
+
287
+ assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
288
+ assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
289
+ sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
290
+
291
+ onChangeIsRefreshing!();
292
+ await waitForAsync();
293
+
294
+ assert.isTrue(services.ready);
295
+ });
296
+
297
+ it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
298
+ services.listenToOnce = sinon.stub();
299
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
300
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
301
+
302
+ services.initialize();
303
+ // initialize() creates a new catalog and replaces the WeakMap entry.
304
+ const currentCatalog = services._getCatalog();
305
+ services.listenToOnce.getCall(0).args[2]();
306
+ services.listenToOnce.getCall(1).args[2]();
307
+ await waitForAsync();
308
+
309
+ const freshLoginCall = services.listenToOnce
310
+ .getCalls()
311
+ .find((call: any) => call.args[1] === 'change:canAuthorize');
312
+ assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
313
+
314
+ services.webex.canAuthorize = true;
315
+ currentCatalog.status.postauth.ready = false;
316
+ freshLoginCall!.args[2]();
317
+
318
+ assert.isTrue(
319
+ services.initServiceCatalogs.called,
320
+ 'expected postauth catalog init after canAuthorize flips'
321
+ );
322
+
323
+ // `catalog.isReady` is now set inside initServiceCatalogs (after the
324
+ // postauth updateServices) rather than by this listener, so it is not
325
+ // asserted here where initServiceCatalogs is stubbed.
326
+ await waitForAsync();
327
+ });
328
+
329
+ it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
330
+ services.listenToOnce = sinon.stub();
331
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
332
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
333
+
334
+ services.initialize();
335
+ // initialize() creates a new catalog; grab the current one.
336
+ const currentCatalog = services._getCatalog();
337
+ services.listenToOnce.getCall(0).args[2]();
338
+ services.listenToOnce.getCall(1).args[2]();
339
+ await waitForAsync();
340
+
341
+ const freshLoginCall = services.listenToOnce
342
+ .getCalls()
343
+ .find((call: any) => call.args[1] === 'change:canAuthorize');
344
+
345
+ services.webex.canAuthorize = true;
346
+ currentCatalog.status.postauth.ready = true;
347
+ freshLoginCall!.args[2]();
348
+
349
+ assert.isFalse(
350
+ services.initServiceCatalogs.called,
351
+ 'must not re-fetch postauth catalog if it is already ready'
352
+ );
353
+ });
354
+
355
+ it('finalizes ready when cache is warm (skips network fetch)', async () => {
356
+ services.listenToOnce = sinon.stub();
357
+ services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
358
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
359
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
360
+
361
+ services.initialize();
362
+ services.listenToOnce.getCall(0).args[2]();
363
+ services.listenToOnce.getCall(1).args[2]();
364
+ await waitForAsync();
365
+ await waitForAsync();
366
+
367
+ assert.isTrue(services.ready);
368
+ assert.isFalse(
369
+ services.initServiceCatalogs.called,
370
+ 'must skip network fetch when cache is warm'
371
+ );
372
+ assert.isFalse(
373
+ services.collectPreauthCatalog.called,
374
+ 'must skip preauth fetch when cache is warm'
375
+ );
376
+ });
377
+ });
378
+
379
+ describe('#_finalizeReady', () => {
380
+ beforeEach(() => {
381
+ // Outer beforeEach construction already ran initialize() (ungated,
382
+ // sets ready=true). Reset so _finalizeReady's flip is observable.
383
+ services.ready = false;
384
+ });
385
+
386
+ it('sets ready=true immediately when credentials are not refreshing', async () => {
387
+ services.webex.credentials = {isRefreshing: false};
388
+ assert.isFalse(services.ready);
389
+
390
+ await services._finalizeReady();
391
+
392
+ assert.isTrue(services.ready);
393
+ });
394
+
395
+ it('sets ready=true when credentials are missing', async () => {
396
+ services.webex.credentials = undefined;
397
+
398
+ await services._finalizeReady();
399
+
400
+ assert.isTrue(services.ready);
401
+ });
402
+
403
+ it('awaits change:isRefreshing before setting ready=true', async () => {
404
+ let onChangeIsRefreshing: (() => void) | undefined;
405
+ services.webex.credentials = {
406
+ isRefreshing: true,
407
+ once: sinon.stub().callsFake((event: string, cb: () => void) => {
408
+ if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
409
+ }),
410
+ };
411
+
412
+ const settled = services._finalizeReady();
413
+ await waitForAsync();
414
+
415
+ assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
416
+ assert.isFunction(onChangeIsRefreshing);
417
+
418
+ onChangeIsRefreshing!();
419
+ await settled;
420
+
421
+ assert.isTrue(services.ready);
274
422
  });
275
423
  });
276
424
 
@@ -425,7 +573,7 @@ describe('webex-core', () => {
425
573
 
426
574
  services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
427
575
  services.webex.credentials = {
428
- getOrgId: sinon.stub().returns(''),
576
+ getOrgId: sinon.stub().returns('')
429
577
  };
430
578
  catalog.status = {};
431
579
  });
@@ -466,7 +614,7 @@ describe('webex-core', () => {
466
614
  const serviceGroup = 'postauth';
467
615
  const hostmap = {services: [{hostmap: 'hostmap'}]};
468
616
 
469
- services._formatReceivedHostmap = sinon.stub().returns({services: [{some: 'hostmap'}]});
617
+ services._formatReceivedHostmap = sinon.stub().returns({services : [{some: 'hostmap'}]});
470
618
 
471
619
  catalog.updateServiceGroups = sinon.stub().returns(Promise.resolve([{some: 'value'}]));
472
620
 
@@ -482,7 +630,7 @@ describe('webex-core', () => {
482
630
  const serviceGroup = 'postauth';
483
631
  const hostmap = {};
484
632
 
485
- services._formatReceivedHostmap = sinon.stub().returns({services: undefined});
633
+ services._formatReceivedHostmap = sinon.stub().returns({services : undefined});
486
634
 
487
635
  catalog.updateServiceGroups = sinon.stub().returns(Promise.resolve([{some: 'value'}]));
488
636
 
@@ -527,6 +675,22 @@ describe('webex-core', () => {
527
675
  );
528
676
  });
529
677
 
678
+ it('includes the catalog override query when configured', async () => {
679
+ services.webex.config.services = {useCatalogOverride: true};
680
+ sinon.stub(services, '_formatReceivedHostmap').resolves('map response');
681
+ sinon.stub(services, 'request').resolves({});
682
+
683
+ await services._fetchNewServiceHostmap({from: 'limited'});
684
+
685
+ assert.calledOnceWithExactly(services.request, {
686
+ method: 'GET',
687
+ service: 'u2c',
688
+ resource: '/limited/catalog',
689
+ qs: {format: 'U2CV2', useCatalogOverride: true},
690
+ headers: {},
691
+ });
692
+ });
693
+
530
694
  it('checks service request rejects', async () => {
531
695
  const error = new Error('some error');
532
696
 
@@ -721,13 +885,13 @@ describe('webex-core', () => {
721
885
  });
722
886
 
723
887
  describe('#invalidateCache', () => {
724
- beforeEach(() => {
888
+ beforeEach( () => {
725
889
  services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
726
890
  services.webex.credentials = {
727
- getOrgId: sinon.stub().returns(''),
891
+ getOrgId: sinon.stub().returns('')
728
892
  };
729
893
  catalog.status = {};
730
- });
894
+ })
731
895
  it('should log the timestamp parameter', async () => {
732
896
  const timestamp = '1234567890';
733
897
  services.logger.info = sinon.stub();
@@ -735,11 +899,7 @@ describe('webex-core', () => {
735
899
 
736
900
  await services.invalidateCache(timestamp);
737
901
 
738
- assert.calledWith(
739
- services.logger.info,
740
- 'services: invalidate cache, timestamp:',
741
- timestamp
742
- );
902
+ assert.calledWith(services.logger.info, 'services: invalidate cache, timestamp:', timestamp);
743
903
  });
744
904
 
745
905
  it('should call initServiceCatalogs when invalidate timestamp is newer than catalog timestamp', async () => {
@@ -866,30 +1026,30 @@ describe('webex-core', () => {
866
1026
  // Arrange: seed internal _services with mobius (including duplicate baseUrl)
867
1027
  services._services = [
868
1028
  {
869
- id: 'urn:TEAM:us-east-2_a:mobius',
870
- serviceName: 'mobius',
871
- serviceUrls: [
872
- {baseUrl: 'https://mobius-us-east-2.prod.infra.webex.com/api/v1', priority: 5},
873
- {baseUrl: 'https://mobius-eu-central-1.prod.infra.webex.com/api/v1', priority: 10},
874
- {baseUrl: 'https://mobius-ap-southeast-2.prod.infra.webex.com/api/v1', priority: 15}, // duplicate
1029
+ "id": "urn:TEAM:us-east-2_a:mobius",
1030
+ "serviceName": 'mobius',
1031
+ "serviceUrls": [
1032
+ {"baseUrl": 'https://mobius-us-east-2.prod.infra.webex.com/api/v1', "priority": 5},
1033
+ {"baseUrl": 'https://mobius-eu-central-1.prod.infra.webex.com/api/v1', "priority": 10},
1034
+ {"baseUrl": 'https://mobius-ap-southeast-2.prod.infra.webex.com/api/v1', "priority": 15}, // duplicate
875
1035
  ],
876
1036
  },
877
1037
  {
878
- id: 'urn:TEAM:ap-southeast-2_m:mobius',
879
- serviceName: 'mobius',
880
- serviceUrls: [
881
- {
882
- baseUrl: 'https://mobius-me-central-1.prod.infra.webex.com/api/v1',
883
- priority: 5,
884
- },
885
- {
886
- baseUrl: 'https://mobius-eu-central-1.prod.infra.webex.com/api/v1',
887
- priority: 10,
888
- },
889
- {
890
- baseUrl: 'https://mobius-ap-southeast-2.prod.infra.webex.com/api/v1',
891
- priority: 15,
892
- },
1038
+ "id": "urn:TEAM:ap-southeast-2_m:mobius",
1039
+ "serviceName": "mobius",
1040
+ "serviceUrls": [
1041
+ {
1042
+ "baseUrl": "https://mobius-me-central-1.prod.infra.webex.com/api/v1",
1043
+ "priority": 5
1044
+ },
1045
+ {
1046
+ "baseUrl": "https://mobius-eu-central-1.prod.infra.webex.com/api/v1",
1047
+ "priority": 10
1048
+ },
1049
+ {
1050
+ "baseUrl": "https://mobius-ap-southeast-2.prod.infra.webex.com/api/v1",
1051
+ "priority": 15
1052
+ },
893
1053
  ],
894
1054
  },
895
1055
  // Non-mobius service should be ignored by getMobiusClusters
@@ -907,64 +1067,42 @@ describe('webex-core', () => {
907
1067
  assert.deepEqual(
908
1068
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
909
1069
  [
910
- {
911
- host: 'mobius-us-east-2.prod.infra.webex.com',
912
- id: 'urn:TEAM:us-east-2_a:mobius',
913
- ttl: 0,
914
- priority: 5,
915
- },
916
- {
917
- host: 'mobius-eu-central-1.prod.infra.webex.com',
918
- id: 'urn:TEAM:us-east-2_a:mobius',
919
- ttl: 0,
920
- priority: 10,
921
- },
922
- {
923
- host: 'mobius-ap-southeast-2.prod.infra.webex.com',
924
- id: 'urn:TEAM:us-east-2_a:mobius',
925
- ttl: 0,
926
- priority: 15,
927
- },
928
- {
929
- host: 'mobius-me-central-1.prod.infra.webex.com',
930
- id: 'urn:TEAM:ap-southeast-2_m:mobius',
931
- ttl: 0,
932
- priority: 5,
933
- },
1070
+ {host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:us-east-2_a:mobius', ttl: 0, priority: 5},
1071
+ {host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:us-east-2_a:mobius', ttl: 0, priority: 10},
1072
+ {host: 'mobius-ap-southeast-2.prod.infra.webex.com', id: 'urn:TEAM:us-east-2_a:mobius', ttl: 0, priority: 15},
1073
+ {host: 'mobius-me-central-1.prod.infra.webex.com', id: 'urn:TEAM:ap-southeast-2_m:mobius', ttl: 0, priority: 5},
934
1074
  ]
935
1075
  );
936
1076
  });
937
1077
  });
938
-
1078
+
939
1079
  describe('#isValidHost', () => {
940
1080
  beforeEach(() => {
941
1081
  // Setting up a mock services list
942
- services._services = [
943
- {
944
- id: 'urn:IDENTITY:PC75:adminAudit',
945
- serviceName: 'adminAudit',
946
- serviceUrls: [
947
- {
948
- baseUrl: 'https://audit-ci-r.wbx2.com/audit-ci/api/v2',
949
- priority: 5,
950
- },
951
- {
952
- baseUrl: 'https://audit-ci-t.wbx2.com/audit-ci/api/v2',
953
- priority: 10,
954
- },
955
- ],
956
- },
957
- {
958
- id: 'urn:IDENTITY:PC75:cdf',
959
- serviceName: 'cdf',
960
- serviceUrls: [
961
- {
962
- baseUrl: 'https://wapdavis.webex.com/davis/api/v1',
963
- priority: 5,
964
- },
965
- ],
966
- },
967
- ];
1082
+ services._services = [{
1083
+ "id": "urn:IDENTITY:PC75:adminAudit",
1084
+ "serviceName": "adminAudit",
1085
+ "serviceUrls": [
1086
+ {
1087
+ "baseUrl": "https://audit-ci-r.wbx2.com/audit-ci/api/v2",
1088
+ "priority": 5
1089
+ },
1090
+ {
1091
+ "baseUrl": "https://audit-ci-t.wbx2.com/audit-ci/api/v2",
1092
+ "priority": 10
1093
+ }
1094
+ ]
1095
+ },
1096
+ {
1097
+ "id": "urn:IDENTITY:PC75:cdf",
1098
+ "serviceName": "cdf",
1099
+ "serviceUrls": [
1100
+ {
1101
+ "baseUrl": "https://wapdavis.webex.com/davis/api/v1",
1102
+ "priority": 5
1103
+ }
1104
+ ]
1105
+ }];
968
1106
  });
969
1107
  afterAll(() => {
970
1108
  // Clean up the mock services list
@@ -988,6 +1126,57 @@ describe('webex-core', () => {
988
1126
  });
989
1127
  });
990
1128
 
1129
+ describe('#isIntegrationEnvironment', () => {
1130
+ it('returns true when u2c URL contains "intb"', () => {
1131
+ services.webex.config = {
1132
+ services: {
1133
+ discovery: {
1134
+ u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
1135
+ },
1136
+ },
1137
+ };
1138
+ assert.isTrue(services.isIntegrationEnvironment());
1139
+ });
1140
+
1141
+ it('returns false when u2c URL does not contain "intb" (production)', () => {
1142
+ services.webex.config = {
1143
+ services: {
1144
+ discovery: {
1145
+ u2c: 'https://u2c.wbx2.com/u2c/api/v1',
1146
+ },
1147
+ },
1148
+ };
1149
+ assert.isFalse(services.isIntegrationEnvironment());
1150
+ });
1151
+
1152
+ it('returns false when u2c URL is for FedRAMP', () => {
1153
+ services.webex.config = {
1154
+ services: {
1155
+ discovery: {
1156
+ u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
1157
+ },
1158
+ },
1159
+ };
1160
+ assert.isFalse(services.isIntegrationEnvironment());
1161
+ });
1162
+
1163
+ it('returns false when u2c URL is undefined', () => {
1164
+ services.webex.config = {
1165
+ services: {
1166
+ discovery: {
1167
+ u2c: undefined,
1168
+ },
1169
+ },
1170
+ };
1171
+ assert.isFalse(services.isIntegrationEnvironment());
1172
+ });
1173
+
1174
+ it('returns false when config is not available', () => {
1175
+ services.webex.config = undefined;
1176
+ assert.isFalse(services.isIntegrationEnvironment());
1177
+ });
1178
+ });
1179
+
991
1180
  describe('U2C catalog cache behavior (v2)', () => {
992
1181
  const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
993
1182
  let windowBackup;