@webex/webex-core 3.11.0 → 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 (46) 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 +217 -31
  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 +221 -30
  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 +192 -17
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +195 -17
  37. package/test/fixtures/activation-email.ts +22 -0
  38. package/test/integration/spec/services/services.js +44 -8
  39. package/test/integration/spec/services-v2/services-v2.js +44 -8
  40. package/test/unit/spec/credentials/credentials.js +133 -2
  41. package/test/unit/spec/interceptors/auth.js +56 -0
  42. package/test/unit/spec/lib/batcher.js +56 -0
  43. package/test/unit/spec/services/service-catalog.js +93 -11
  44. package/test/unit/spec/services/services.js +358 -2
  45. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  46. package/test/unit/spec/services-v2/services-v2.ts +362 -0
@@ -101,7 +101,24 @@ describe('webex-core', () => {
101
101
  const domains = [];
102
102
 
103
103
  beforeEach(() => {
104
- domains.push('example-a', 'example-b', 'example-c');
104
+ // Shaped like a real catalog rather than a tidy list: the commercial
105
+ // domains the sdk ships with, a multi-part suffix, sites added at
106
+ // runtime from meeting preferences, an entry that overlaps another,
107
+ // and an unset entry, which callers can add and which must match
108
+ // nothing.
109
+ domains.push(
110
+ 'wbx2.com',
111
+ 'ciscospark.com',
112
+ 'webex.com',
113
+ 'webexapis.com',
114
+ 'broadcloud.com.au',
115
+ 'webexgov.us',
116
+ 'example-a.com',
117
+ 'example-b.com',
118
+ 'example-c.com',
119
+ 'go.example-a.com',
120
+ ''
121
+ );
105
122
 
106
123
  catalog.setAllowedDomains(domains);
107
124
  });
@@ -110,10 +127,60 @@ describe('webex-core', () => {
110
127
  domains.length = 0;
111
128
  });
112
129
 
113
- it('finds an allowed domain that matches a specific url', () => {
114
- const domain = catalog.findAllowedDomain('http://example-a.com/resource/id');
115
-
116
- assert.include(domains, domain);
130
+ [
131
+ // real service hosts resolve to the entry that covers them
132
+ ['https://u2c-a.wbx2.com/u2c/api/v1/limited/catalog', 'wbx2.com'],
133
+ ['https://conv-a.wbx2.com/conversation/api/v1/conversations', 'wbx2.com'],
134
+ ['https://foobar.ciscospark.com/resource/id', 'ciscospark.com'],
135
+ ['https://idbroker.webex.com/idb/token', 'webex.com'],
136
+ ['https://webexapis.com/v1/people/me', 'webexapis.com'],
137
+ // an entry is not required to be a bare registrable domain
138
+ ['https://foo.broadcloud.com.au/resource/id', 'broadcloud.com.au'],
139
+ ['https://a.b.webexgov.us/resource/id', 'webexgov.us'],
140
+ // the domain itself and its subdomains match
141
+ ['https://example-a.com/resource/id', 'example-a.com'],
142
+ ['https://sub.example-b.com/resource/id', 'example-b.com'],
143
+ ['https://deep.sub.example-c.com/resource/id', 'example-c.com'],
144
+ // overlapping entries resolve to the first covering entry in the list
145
+ ['https://go.example-a.com/resource/id', 'example-a.com'],
146
+ // an explicit port must not defeat the match
147
+ ['https://example-a.com:8443/resource/id', 'example-a.com'],
148
+ ['https://u2c-a.wbx2.com:8000/resource/id', 'wbx2.com'],
149
+ // hostname comparison is case insensitive
150
+ ['https://U2C-A.WBX2.COM/resource/id', 'wbx2.com'],
151
+ // a trailing dot is the same name
152
+ ['https://u2c-a.wbx2.com./resource/id', 'wbx2.com'],
153
+ // a sibling registrable domain must not match
154
+ ['https://notwebex.com/resource/id', undefined],
155
+ ['https://mywebexgov.us/resource/id', undefined],
156
+ ['https://webex.company/resource/id', undefined],
157
+ ['https://webex.com-unrelated.example/resource/id', undefined],
158
+ ['https://example-a.community/resource/id', undefined],
159
+ // a partial label must not match, even against a multi-part entry
160
+ ['https://broadcloud.com/resource/id', undefined],
161
+ // the domain matches only as a suffix, on a label boundary
162
+ ['https://webex.com.unrelated.example/resource/id', undefined],
163
+ ['https://unrelated.example/webex.com/resource/id', undefined],
164
+ ['https://unrelated.example/?next=https://webex.com', undefined],
165
+ // userinfo is not the host
166
+ ['https://webex.com@unrelated.example/resource/id', undefined],
167
+ // an encoded or unusual separator is not a label boundary, and the url
168
+ // parsers used by the transports do not agree on where these end the
169
+ // host, so they must not resolve to an allowed domain
170
+ ['https://webex.com%2eunrelated.example/resource/id', undefined],
171
+ ['https://webex.com%2Eunrelated.example/resource/id', undefined],
172
+ ['https://unrelated.example%2ewebex.com/resource/id', undefined],
173
+ ['https://unrelated.example%2Ewebex.com/resource/id', undefined],
174
+ ['https://unrelated.example;.webex.com/resource/id', undefined],
175
+ // an empty allowed domain entry matches nothing
176
+ ['https://unrelated.example/resource/id', undefined],
177
+ // unparseable urls
178
+ ['', undefined],
179
+ ['not a url', undefined],
180
+ ].forEach(([url, expected]) => {
181
+ it(`returns ${expected} for ${url || '<empty>'}`, () => {
182
+ assert.equal(catalog.findAllowedDomain(url), expected);
183
+ });
117
184
  });
118
185
  });
119
186
 
@@ -121,7 +188,7 @@ describe('webex-core', () => {
121
188
  const domains = [];
122
189
 
123
190
  beforeEach(() => {
124
- domains.push('example-a', 'example-b', 'example-c');
191
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
125
192
 
126
193
  catalog.setAllowedDomains(domains);
127
194
  });
@@ -141,7 +208,7 @@ describe('webex-core', () => {
141
208
  const domains = [];
142
209
 
143
210
  beforeEach(() => {
144
- domains.push('example-a', 'example-b', 'example-c');
211
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
145
212
 
146
213
  catalog.setAllowedDomains(domains);
147
214
  });
@@ -151,19 +218,31 @@ describe('webex-core', () => {
151
218
  });
152
219
 
153
220
  it('sets the allowed domain entries to new values', () => {
154
- const newValues = ['example-d', 'example-e', 'example-f'];
221
+ const newValues = ['example-d.com', 'example-e.com', 'example-f.com'];
155
222
 
156
223
  catalog.setAllowedDomains(newValues);
157
224
 
158
225
  assert.notDeepInclude(domains, newValues);
159
226
  });
227
+
228
+ it('canonicalizes entries and discards those that are not usable', () => {
229
+ catalog.setAllowedDomains([
230
+ 'Example-D.COM',
231
+ 'example-d.com',
232
+ 'example-e.com.',
233
+ '',
234
+ undefined,
235
+ ]);
236
+
237
+ assert.deepEqual(catalog.getAllowedDomains(), ['example-d.com', 'example-e.com']);
238
+ });
160
239
  });
161
240
 
162
241
  describe('#addAllowedDomains()', () => {
163
242
  const domains = [];
164
243
 
165
244
  beforeEach(() => {
166
- domains.push('example-a', 'example-b', 'example-c');
245
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
167
246
 
168
247
  catalog.setAllowedDomains(domains);
169
248
  });
@@ -173,13 +252,16 @@ describe('webex-core', () => {
173
252
  });
174
253
 
175
254
  it('merge the allowed domain entries with new values', () => {
176
- const newValues = ['example-c', 'example-e', 'example-f'];
255
+ const newValues = ['example-c.com', 'example-e.com', 'example-f.com'];
177
256
 
178
257
  catalog.addAllowedDomains(newValues);
179
258
 
180
259
  const list = catalog.getAllowedDomains();
181
260
 
182
- assert.match(['example-a', 'example-b', 'example-c', 'example-e', 'example-f'], list);
261
+ assert.match(
262
+ ['example-a.com', 'example-b.com', 'example-c.com', 'example-e.com', 'example-f.com'],
263
+ list
264
+ );
183
265
  });
184
266
  });
185
267
 
@@ -46,6 +46,7 @@ describe('webex-core', () => {
46
46
  services.initialize();
47
47
 
48
48
  // call the onReady callback
49
+ services.listenToOnce.getCall(0).args[2]();
49
50
  services.listenToOnce.getCall(1).args[2]();
50
51
  await waitForAsync();
51
52
 
@@ -59,6 +60,7 @@ describe('webex-core', () => {
59
60
  services.initialize();
60
61
 
61
62
  // call the onReady callback
63
+ services.listenToOnce.getCall(0).args[2]();
62
64
  services.listenToOnce.getCall(1).args[2]();
63
65
  await waitForAsync();
64
66
 
@@ -81,6 +83,7 @@ describe('webex-core', () => {
81
83
  services.initialize();
82
84
 
83
85
  // call the onReady callback
86
+ services.listenToOnce.getCall(0).args[2]();
84
87
  services.listenToOnce.getCall(1).args[2]();
85
88
 
86
89
  await waitForAsync();
@@ -114,6 +117,7 @@ describe('webex-core', () => {
114
117
  services.initialize();
115
118
 
116
119
  // call the onReady callback
120
+ services.listenToOnce.getCall(0).args[2]();
117
121
  services.listenToOnce.getCall(1).args[2]();
118
122
 
119
123
  await waitForAsync();
@@ -125,6 +129,297 @@ describe('webex-core', () => {
125
129
  );
126
130
  }
127
131
  );
132
+
133
+ it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
134
+ services.listenToOnce = sinon.stub();
135
+ services.initialize();
136
+
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]();
140
+
141
+ assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
142
+ });
143
+
144
+ it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
145
+ services.listenToOnce = sinon.stub();
146
+ services.initialize();
147
+
148
+ // Fire change:config to trigger the mode-specific listener registration.
149
+ services.listenToOnce.getCall(0).args[2]();
150
+
151
+ const [, event] = services.listenToOnce.getCall(1).args;
152
+ assert.equal(event, 'ready', 'default path listens on webex ready');
153
+ });
154
+ });
155
+
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
+ });
168
+
169
+ it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
170
+ services.listenToOnce = sinon.stub();
171
+ services.initialize();
172
+
173
+ assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
174
+ });
175
+
176
+ it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
177
+ services.listenToOnce = sinon.stub();
178
+ services.initialize();
179
+
180
+ // Fire change:config to trigger the mode-specific listener registration.
181
+ services.listenToOnce.getCall(0).args[2]();
182
+
183
+ const [, event] = services.listenToOnce.getCall(1).args;
184
+ assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
185
+ });
186
+
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
+ };
193
+
194
+ services.initialize();
195
+ services.listenToOnce.getCall(0).args[2]();
196
+ services.listenToOnce.getCall(1).args[2]();
197
+ await waitForAsync();
198
+
199
+ assert.isTrue(services.ready);
200
+ });
201
+
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());
205
+
206
+ services.initialize();
207
+ services.listenToOnce.getCall(0).args[2]();
208
+ services.listenToOnce.getCall(1).args[2]();
209
+ await waitForAsync();
210
+
211
+ assert.isTrue(services.ready);
212
+ });
213
+
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();
221
+
222
+ services.initialize();
223
+ services.listenToOnce.getCall(0).args[2]();
224
+ services.listenToOnce.getCall(1).args[2]();
225
+ await waitForAsync();
226
+
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);
422
+ });
128
423
  });
129
424
 
130
425
  describe('#initServiceCatalogs', () => {
@@ -380,6 +675,22 @@ describe('webex-core', () => {
380
675
  );
381
676
  });
382
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
+
383
694
  it('checks service request rejects', async () => {
384
695
  const error = new Error('some error');
385
696
 
@@ -815,6 +1126,57 @@ describe('webex-core', () => {
815
1126
  });
816
1127
  });
817
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
+
818
1180
  describe('U2C catalog cache behavior (v2)', () => {
819
1181
  const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
820
1182
  let windowBackup;