@webex/webex-core 3.12.0-next.9 → 3.12.0-webex-services-ready.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.
@@ -34,6 +34,53 @@ 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
+
37
84
  it('initFailed is false when initialization succeeds and credentials are available', async () => {
38
85
  services.listenToOnce = sinon.stub();
39
86
  services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
@@ -45,7 +92,7 @@ describe('webex-core', () => {
45
92
 
46
93
  services.initialize();
47
94
 
48
- // call the onReady callback
95
+ // call the onLoaded callback
49
96
  services.listenToOnce.getCall(1).args[2]();
50
97
  await waitForAsync();
51
98
 
@@ -58,7 +105,7 @@ describe('webex-core', () => {
58
105
 
59
106
  services.initialize();
60
107
 
61
- // call the onReady callback
108
+ // call the onLoaded callback
62
109
  services.listenToOnce.getCall(1).args[2]();
63
110
  await waitForAsync();
64
111
 
@@ -67,9 +114,9 @@ describe('webex-core', () => {
67
114
 
68
115
  it.each([
69
116
  {error: new Error('failed'), expectedMessage: 'failed'},
70
- {error: undefined, expectedMessage: undefined}
117
+ {error: undefined, expectedMessage: undefined},
71
118
  ])(
72
- 'sets initFailed to true when collectPreauthCatalog errors',
119
+ 'sets initFailed to true when collectPreauthCatalog errors but still sets ready to true',
73
120
  async ({error, expectedMessage}) => {
74
121
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
75
122
  return Promise.reject(error);
@@ -77,15 +124,18 @@ describe('webex-core', () => {
77
124
 
78
125
  services.listenToOnce = sinon.stub();
79
126
  services.logger.error = sinon.stub();
127
+ services.trigger = sinon.stub();
80
128
 
81
129
  services.initialize();
82
130
 
83
- // call the onReady callback
131
+ // call the onLoaded callback
84
132
  services.listenToOnce.getCall(1).args[2]();
85
133
 
86
134
  await waitForAsync();
87
135
 
88
136
  assert.isTrue(services.initFailed);
137
+ assert.isTrue(services.ready);
138
+ sinon.assert.calledWith(services.trigger, 'services:initialized');
89
139
  sinon.assert.calledWith(
90
140
  services.logger.error,
91
141
  `services: failed to init initial services when no credentials available, ${expectedMessage}`
@@ -95,32 +145,132 @@ describe('webex-core', () => {
95
145
 
96
146
  it.each([
97
147
  {error: new Error('failed'), expectedMessage: 'failed'},
98
- {error: undefined, expectedMessage: undefined}
99
- ])('sets initFailed to true when initServiceCatalogs errors', async ({error, expectedMessage}) => {
100
- services.initServiceCatalogs = sinon.stub().callsFake(() => {
101
- return Promise.reject(error);
102
- });
103
- services.webex.credentials = {
104
- supertoken: {
105
- access_token: 'token'
106
- }
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
+ };
160
+
161
+ services.listenToOnce = sinon.stub();
162
+ services.logger.error = sinon.stub();
163
+ services.trigger = sinon.stub();
164
+
165
+ services.initialize();
166
+
167
+ // call the onLoaded callback
168
+ services.listenToOnce.getCall(1).args[2]();
169
+
170
+ await waitForAsync();
171
+
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
+ );
107
179
  }
180
+ );
108
181
 
109
- services.listenToOnce = sinon.stub();
110
- services.logger.error = sinon.stub();
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 = {};
111
191
 
112
- services.initialize();
192
+ services.initialize();
113
193
 
114
- // call the onReady callback
115
- services.listenToOnce.getCall(1).args[2]();
194
+ // Get the catalog after initialize creates it
195
+ const testCatalog = services._getCatalog();
196
+ testCatalog.status.postauth.ready = false;
116
197
 
117
- await waitForAsync();
198
+ // call the onLoaded callback (preauth path)
199
+ services.listenToOnce.getCall(1).args[2]();
200
+ await waitForAsync();
118
201
 
119
- assert.isTrue(services.initFailed);
120
- sinon.assert.calledWith(
121
- services.logger.error,
122
- `services: failed to init initial services when credentials available, ${expectedMessage}`
123
- );
202
+ // Now set canAuthorize to true to simulate auth completing
203
+ services.webex.canAuthorize = true;
204
+
205
+ // call the change:canAuthorize callback (should be call 2)
206
+ services.listenToOnce.getCall(2).args[2]();
207
+ await waitForAsync();
208
+
209
+ sinon.assert.calledOnce(services.initServiceCatalogs);
210
+ assert.isTrue(testCatalog.isReady);
211
+ });
212
+
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 = {};
221
+
222
+ services.initialize();
223
+
224
+ // Get the catalog after initialize creates it
225
+ const testCatalog = services._getCatalog();
226
+
227
+ // call the onLoaded callback (preauth path)
228
+ services.listenToOnce.getCall(1).args[2]();
229
+ await waitForAsync();
230
+
231
+ // Set canAuthorize to true and postauth as ready BEFORE calling the callback
232
+ services.webex.canAuthorize = true;
233
+ testCatalog.status.postauth.ready = true;
234
+
235
+ // call the change:canAuthorize callback (should be call 2)
236
+ services.listenToOnce.getCall(2).args[2]();
237
+ await waitForAsync();
238
+
239
+ sinon.assert.notCalled(services.initServiceCatalogs);
240
+ });
241
+
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 = {};
251
+
252
+ services.initialize();
253
+
254
+ // Get the catalog after initialize creates it
255
+ const testCatalog = services._getCatalog();
256
+ testCatalog.status.postauth.ready = false;
257
+
258
+ // call the onLoaded callback (preauth path)
259
+ services.listenToOnce.getCall(1).args[2]();
260
+ await waitForAsync();
261
+
262
+ // Now set canAuthorize to true to simulate auth completing
263
+ services.webex.canAuthorize = true;
264
+
265
+ // call the change:canAuthorize callback (should be call 2)
266
+ services.listenToOnce.getCall(2).args[2]();
267
+ await waitForAsync();
268
+
269
+ sinon.assert.calledWith(
270
+ services.logger.error,
271
+ 'services: failed to init service catalogs after auth, auth failed'
272
+ );
273
+ });
124
274
  });
125
275
  });
126
276
 
@@ -159,7 +309,7 @@ describe('webex-core', () => {
159
309
 
160
310
  services.collectPreauthCatalog = sinon.stub().callsFake(() => {
161
311
  return Promise.resolve();
162
- })
312
+ });
163
313
 
164
314
  services.updateServices = sinon.stub().callsFake(() => {
165
315
  return Promise.reject(error);
@@ -254,7 +404,7 @@ describe('webex-core', () => {
254
404
  discovery: {
255
405
  sqdiscovery: 'https://test.ciscospark.com/v1/region',
256
406
  },
257
- }
407
+ },
258
408
  };
259
409
  });
260
410
 
@@ -274,8 +424,8 @@ describe('webex-core', () => {
274
424
  assert.calledWith(webex.request, {
275
425
  uri: 'https://test.ciscospark.com/v1/region',
276
426
  addAuthHeader: false,
277
- headers: { 'spark-user-agent': null },
278
- timeout: 5000
427
+ headers: {'spark-user-agent': null},
428
+ timeout: 5000,
279
429
  });
280
430
  });
281
431
  });
@@ -332,7 +482,6 @@ describe('webex-core', () => {
332
482
  });
333
483
 
334
484
  describe('#_fetchNewServiceHostmap()', () => {
335
-
336
485
  beforeEach(() => {
337
486
  sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
338
487
  });
@@ -346,17 +495,20 @@ describe('webex-core', () => {
346
495
 
347
496
  sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
348
497
  sinon.stub(services, 'request').resolves({});
349
-
498
+
350
499
  const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
351
500
 
352
501
  assert.calledOnceWithExactly(services.request, {
353
502
  method: 'GET',
354
503
  service: 'u2c',
355
504
  resource: '/limited/catalog',
356
- qs: {format: 'hostmap'}
357
- }
505
+ qs: {format: 'hostmap'},
506
+ });
507
+ assert.calledOnceWithExactly(
508
+ webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
509
+ sinon.match.func,
510
+ 'internal.get.u2c.time'
358
511
  );
359
- assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
360
512
  });
361
513
 
362
514
  it('checks service request rejects', async () => {
@@ -364,7 +516,7 @@ describe('webex-core', () => {
364
516
 
365
517
  sinon.spy(services, '_formatReceivedHostmap');
366
518
  sinon.stub(services, 'request').rejects(error);
367
-
519
+
368
520
  const promise = services._fetchNewServiceHostmap({from: 'limited'});
369
521
  const rejectedValue = await assert.isRejected(promise);
370
522
 
@@ -376,10 +528,13 @@ describe('webex-core', () => {
376
528
  method: 'GET',
377
529
  service: 'u2c',
378
530
  resource: '/limited/catalog',
379
- qs: {format: 'hostmap'}
380
- }
531
+ qs: {format: 'hostmap'},
532
+ });
533
+ assert.calledOnceWithExactly(
534
+ webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
535
+ sinon.match.func,
536
+ 'internal.get.u2c.time'
381
537
  );
382
- assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
383
538
  });
384
539
  });
385
540
 
@@ -410,7 +565,6 @@ describe('webex-core', () => {
410
565
  });
411
566
 
412
567
  it('returns the original uri if the hostmap has no hosts for the host', () => {
413
-
414
568
  services._hostCatalog = {
415
569
  'example.com': [],
416
570
  };
@@ -574,8 +728,7 @@ describe('webex-core', () => {
574
728
  id: '0:0:0:different-e-x',
575
729
  },
576
730
  ],
577
- 'example-f.com': [
578
- ],
731
+ 'example-f.com': [],
579
732
  },
580
733
  format: 'hostmap',
581
734
  };
@@ -783,7 +936,7 @@ describe('webex-core', () => {
783
936
  defaultUrl: 'https://example-g.com/api/v1',
784
937
  hosts: [],
785
938
  name: 'example-g',
786
- }
939
+ },
787
940
  ]);
788
941
  });
789
942
 
@@ -837,34 +990,59 @@ describe('webex-core', () => {
837
990
  assert.equal(webex.config.credentials.authorizeUrl, authUrl);
838
991
  });
839
992
  });
840
-
993
+
841
994
  describe('#getMobiusClusters', () => {
842
995
  it('returns unique mobius host entries from hostCatalog', () => {
843
996
  // Arrange: two hostCatalog keys, with duplicate mobius host across keys
844
997
  services._hostCatalog = {
845
998
  'mobius-us-east-2.prod.infra.webex.com': [
846
- {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
847
- {host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
848
- ],
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
+ ],
849
1012
 
850
1013
  'mobius-eu-central-1.prod.infra.webex.com': [
851
- {host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
852
- ],
853
- 'wdm-a.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': [
854
1022
  {host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
855
- ]
1023
+ ],
856
1024
  };
857
-
1025
+
858
1026
  // Act
859
1027
  const clusters = services.getMobiusClusters();
860
-
1028
+
861
1029
  // Assert
862
1030
  // deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
863
1031
  assert.deepEqual(
864
1032
  clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
865
1033
  [
866
- {host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
867
- {host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
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
+ },
868
1046
  ]
869
1047
  );
870
1048
  });
@@ -873,31 +1051,31 @@ describe('webex-core', () => {
873
1051
  describe('#isValidHost', () => {
874
1052
  beforeEach(() => {
875
1053
  // Setting up a mock host catalog
876
- services._hostCatalog = {
877
- "audit-ci-m.wbx2.com": [
878
- {
879
- "host": "audit-ci-m.wbx2.com",
880
- "ttl": -1,
881
- "priority": 5,
882
- "id": "urn:IDENTITY:PA61:adminAudit"
883
- },
884
- {
885
- "host": "audit-ci-m.wbx2.com",
886
- "ttl": -1,
887
- "priority": 5,
888
- "id": "urn:IDENTITY:PA61:adminAuditV2"
889
- }
890
- ],
891
- "mercury-connection-partition0-r.wbx2.com": [
892
- {
893
- "host": "mercury-connection-partition0-r.wbx2.com",
894
- "ttl": -1,
895
- "priority": 5,
896
- "id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
897
- }
898
- ],
899
- "empty.com": []
900
- };
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
+ };
901
1079
  });
902
1080
  afterAll(() => {
903
1081
  // Clean up the mock host catalog
@@ -979,7 +1157,7 @@ describe('webex-core', () => {
979
1157
  let catalog;
980
1158
  let localStorageBackup;
981
1159
  let windowBackup;
982
-
1160
+
983
1161
  const makeLocalStorageShim = () => {
984
1162
  const store = new Map();
985
1163
  return {
@@ -989,13 +1167,16 @@ describe('webex-core', () => {
989
1167
  _store: store,
990
1168
  };
991
1169
  };
992
-
1170
+
993
1171
  beforeEach(() => {
994
1172
  // Build a fresh webex instance
995
- webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
1173
+ webex = new MockWebex({
1174
+ children: {services: Services},
1175
+ config: {credentials: {federation: true}},
1176
+ });
996
1177
  services = webex.internal.services;
997
1178
  catalog = services._getCatalog();
998
-
1179
+
999
1180
  // enable U2C caching feature flag in tests that rely on localStorage writes/reads
1000
1181
  services.webex.config = services.webex.config || {};
1001
1182
  services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
@@ -1007,13 +1188,15 @@ describe('webex-core', () => {
1007
1188
  global.window.localStorage = makeLocalStorageShim();
1008
1189
  // Ensure code under test uses our shim via util method
1009
1190
  sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
1010
-
1191
+
1011
1192
  // Stub the formatter so we don't need a full hostmap payload in tests
1012
- sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1013
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1014
- ]);
1193
+ sinon
1194
+ .stub(services, '_formatReceivedHostmap')
1195
+ .callsFake(() => [
1196
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1197
+ ]);
1015
1198
  });
1016
-
1199
+
1017
1200
  afterEach(() => {
1018
1201
  global.window.localStorage = localStorageBackup || undefined;
1019
1202
  if (!windowBackup) {
@@ -1026,7 +1209,7 @@ describe('webex-core', () => {
1026
1209
  services._getLocalStorageSafe.restore();
1027
1210
  }
1028
1211
  });
1029
-
1212
+
1030
1213
  it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
1031
1214
  // Arrange: authenticated credentials and spies
1032
1215
  services.webex.credentials = {
@@ -1038,10 +1221,20 @@ describe('webex-core', () => {
1038
1221
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1039
1222
  const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
1040
1223
  // Make fetch return a hostmap object and allow formatter to reduce it
1041
- sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
1042
- // Cause ready callback to run immediately
1224
+ sinon
1225
+ .stub(services, 'request')
1226
+ .resolves({
1227
+ body: {
1228
+ services: [],
1229
+ activeServices: {},
1230
+ timestamp: Date.now().toString(),
1231
+ orgId: 'urn:EXAMPLE:org',
1232
+ format: 'U2CV2',
1233
+ },
1234
+ });
1235
+ // Cause loaded callback to run immediately
1043
1236
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1044
- if (event === 'ready') cb();
1237
+ if (event === 'loaded') cb();
1045
1238
  });
1046
1239
 
1047
1240
  // Act
@@ -1081,9 +1274,9 @@ describe('webex-core', () => {
1081
1274
 
1082
1275
  const initSpy = sinon.spy(services, 'initServiceCatalogs');
1083
1276
  const cacheSpy = sinon.spy(services, '_cacheCatalog');
1084
- // Cause ready callback to run immediately
1277
+ // Cause loaded callback to run immediately
1085
1278
  services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
1086
- if (event === 'ready') cb();
1279
+ if (event === 'loaded') cb();
1087
1280
  });
1088
1281
 
1089
1282
  // Act
@@ -1091,9 +1284,18 @@ describe('webex-core', () => {
1091
1284
  await waitForAsync();
1092
1285
 
1093
1286
  // Assert: ready path found cache and skipped initServiceCatalogs
1094
- assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
1095
- assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
1096
- assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
1287
+ assert.isFalse(
1288
+ initSpy.called,
1289
+ 'expected initServiceCatalogs to be skipped with cache present'
1290
+ );
1291
+ assert.isTrue(
1292
+ services._getCatalog().status.preauth.ready,
1293
+ 'preauth should be ready from cache'
1294
+ );
1295
+ assert.isTrue(
1296
+ services._getCatalog().status.postauth.ready,
1297
+ 'postauth should be ready from cache'
1298
+ );
1097
1299
  assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
1098
1300
 
1099
1301
  // Cleanup
@@ -1109,26 +1311,29 @@ describe('webex-core', () => {
1109
1311
  preauth: {serviceLinks: {}, hostCatalog: {}},
1110
1312
  postauth: {serviceLinks: {}, hostCatalog: {}},
1111
1313
  };
1112
-
1314
+
1113
1315
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
1114
-
1316
+
1115
1317
  const warmed = await services._loadCatalogFromCache();
1116
-
1318
+
1117
1319
  assert.isFalse(warmed, 'stale cache must not warm');
1118
- assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
1320
+ assert.isNull(
1321
+ window.localStorage.getItem(CATALOG_CACHE_KEY_V1),
1322
+ 'expired cache must be cleared'
1323
+ );
1119
1324
  assert.isFalse(catalog.status.preauth.ready);
1120
1325
  assert.isFalse(catalog.status.postauth.ready);
1121
1326
  });
1122
-
1327
+
1123
1328
  it('clearCatalogCache() removes the cached entry', async () => {
1124
1329
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1125
1330
  window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
1126
-
1331
+
1127
1332
  await services.clearCatalogCache();
1128
-
1333
+
1129
1334
  assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
1130
1335
  });
1131
-
1336
+
1132
1337
  it('still fetches when forceRefresh=true even if ready', async () => {
1133
1338
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
1134
1339
  window.localStorage.setItem(
@@ -1140,20 +1345,24 @@ describe('webex-core', () => {
1140
1345
  postauth: {serviceLinks: {}, hostCatalog: {}},
1141
1346
  })
1142
1347
  );
1143
-
1348
+
1144
1349
  // warm from cache
1145
1350
  const warmed = await services._loadCatalogFromCache();
1146
1351
  assert.isTrue(warmed);
1147
1352
  assert.isTrue(catalog.status.preauth.ready);
1148
1353
  assert.isTrue(catalog.status.postauth.ready);
1149
-
1354
+
1150
1355
  const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
1151
-
1356
+
1152
1357
  // with forceRefresh we should fetch despite ready=true
1153
- await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
1358
+ await services.updateServices({
1359
+ from: 'limited',
1360
+ query: {orgId: 'urn:EXAMPLE:org'},
1361
+ forceRefresh: true,
1362
+ });
1154
1363
  // pass an empty query to avoid spreading undefined in qs construction
1155
1364
  await services.updateServices({forceRefresh: true});
1156
-
1365
+
1157
1366
  assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
1158
1367
  fetchSpy.restore();
1159
1368
  });
@@ -1214,9 +1423,11 @@ describe('webex-core', () => {
1214
1423
  );
1215
1424
  // formatter returns at least one entry to mark ready
1216
1425
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1217
- sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1218
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1219
- ]);
1426
+ sinon
1427
+ .stub(services, '_formatReceivedHostmap')
1428
+ .callsFake(() => [
1429
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1430
+ ]);
1220
1431
 
1221
1432
  const warmed = await services._loadCatalogFromCache();
1222
1433
  assert.isTrue(warmed);
@@ -1238,9 +1449,11 @@ describe('webex-core', () => {
1238
1449
  })
1239
1450
  );
1240
1451
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1241
- sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1242
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1243
- ]);
1452
+ sinon
1453
+ .stub(services, '_formatReceivedHostmap')
1454
+ .callsFake(() => [
1455
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1456
+ ]);
1244
1457
 
1245
1458
  const warmed = await services._loadCatalogFromCache();
1246
1459
  // function returns true if overall cache path succeeded; we only verify group readiness
@@ -1267,12 +1480,17 @@ describe('webex-core', () => {
1267
1480
  })
1268
1481
  );
1269
1482
  services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
1270
- sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
1271
- {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1272
- ]);
1483
+ sinon
1484
+ .stub(services, '_formatReceivedHostmap')
1485
+ .callsFake(() => [
1486
+ {name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
1487
+ ]);
1273
1488
 
1274
1489
  await services._loadCatalogFromCache();
1275
- assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
1490
+ assert.isFalse(
1491
+ catalog.status.preauth.ready,
1492
+ 'preauth should not warm on selection mismatch'
1493
+ );
1276
1494
  });
1277
1495
 
1278
1496
  it('skips warming when environment fingerprint mismatches', async () => {
@@ -1283,7 +1501,10 @@ describe('webex-core', () => {
1283
1501
  JSON.stringify({
1284
1502
  cachedAt: Date.now(),
1285
1503
  env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
1286
- preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
1504
+ preauth: {
1505
+ hostMap: {serviceLinks: {}, hostCatalog: {}},
1506
+ meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
1507
+ },
1287
1508
  })
1288
1509
  );
1289
1510
  // current env
@@ -1298,7 +1519,6 @@ describe('webex-core', () => {
1298
1519
  assert.isFalse(catalog.status.postauth.ready);
1299
1520
  });
1300
1521
  });
1301
-
1302
1522
  });
1303
1523
  });
1304
1524
  /* eslint-enable no-underscore-dangle */