@webex/webex-core 3.12.0-next.32 → 3.12.0-next.33
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.
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/batcher.js +1 -1
- package/dist/lib/credentials/credentials.js +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/services/services.js +196 -28
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +195 -26
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/package.json +3 -3
- package/src/config.js +10 -0
- package/src/lib/services/services.js +166 -17
- package/src/lib/services-v2/services-v2.ts +166 -16
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/services.js +44 -8
- package/test/integration/spec/services-v2/services-v2.js +44 -8
- package/test/unit/spec/services/services.js +307 -2
- package/test/unit/spec/services-v2/services-v2.ts +295 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
formattedServiceHostmapEntryTest,
|
|
26
26
|
serviceHostmapV2,
|
|
27
27
|
} from '../../../fixtures/host-catalog-v2';
|
|
28
|
+
import {createActivationEmail} from '../../../fixtures/activation-email';
|
|
28
29
|
|
|
29
30
|
// /* eslint-disable no-underscore-dangle */
|
|
30
31
|
describe('webex-core', () => {
|
|
@@ -320,11 +321,13 @@ describe('webex-core', () => {
|
|
|
320
321
|
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
321
322
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
322
323
|
services.initialize();
|
|
324
|
+
// The mode-specific ('ready'/'loaded') listener is registered inside the
|
|
325
|
+
// change:config handler, so fire change:config first, then 'ready'.
|
|
326
|
+
webex.trigger('change:config');
|
|
323
327
|
webex.trigger('ready');
|
|
324
328
|
// Wait for the async 'ready' handler to complete
|
|
325
329
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
326
330
|
assert.called(services.initServiceCatalogs);
|
|
327
|
-
assert.isTrue(catalog.isReady);
|
|
328
331
|
});
|
|
329
332
|
|
|
330
333
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -341,6 +344,39 @@ describe('webex-core', () => {
|
|
|
341
344
|
done();
|
|
342
345
|
}, 2000);
|
|
343
346
|
});
|
|
347
|
+
|
|
348
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
349
|
+
const gatedWebex = new WebexCore({
|
|
350
|
+
credentials: {supertoken: webexUser.token},
|
|
351
|
+
config: {services: {waitForCatalogInit: true}},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
355
|
+
// is a dependency and starts false in the gated path.
|
|
356
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
357
|
+
assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
|
|
358
|
+
|
|
359
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
360
|
+
await new Promise((resolve, reject) => {
|
|
361
|
+
if (gatedWebex.internal.services.ready) {
|
|
362
|
+
resolve();
|
|
363
|
+
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const timer = setTimeout(
|
|
367
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
368
|
+
30_000
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
resolve();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
|
|
378
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
379
|
+
});
|
|
344
380
|
});
|
|
345
381
|
|
|
346
382
|
describe('#initServiceCatalogs()', () => {
|
|
@@ -815,7 +851,7 @@ describe('webex-core', () => {
|
|
|
815
851
|
|
|
816
852
|
it('validates a non-existing user', () =>
|
|
817
853
|
unauthServices
|
|
818
|
-
.validateUser({email:
|
|
854
|
+
.validateUser({email: createActivationEmail()})
|
|
819
855
|
.then((r) => {
|
|
820
856
|
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
821
857
|
assert.equal(r.activated, false);
|
|
@@ -825,7 +861,7 @@ describe('webex-core', () => {
|
|
|
825
861
|
it('validates new user with activationOptions suppressEmail false', () =>
|
|
826
862
|
unauthServices
|
|
827
863
|
.validateUser({
|
|
828
|
-
email:
|
|
864
|
+
email: createActivationEmail(),
|
|
829
865
|
activationOptions: {suppressEmail: false},
|
|
830
866
|
})
|
|
831
867
|
.then((r) => {
|
|
@@ -838,7 +874,7 @@ describe('webex-core', () => {
|
|
|
838
874
|
it('validates new user with activationOptions suppressEmail true', () =>
|
|
839
875
|
unauthServices
|
|
840
876
|
.validateUser({
|
|
841
|
-
email:
|
|
877
|
+
email: createActivationEmail(),
|
|
842
878
|
activationOptions: {suppressEmail: true},
|
|
843
879
|
})
|
|
844
880
|
.then((r) => {
|
|
@@ -882,7 +918,7 @@ describe('webex-core', () => {
|
|
|
882
918
|
|
|
883
919
|
return unauthServices
|
|
884
920
|
.validateUser({
|
|
885
|
-
email:
|
|
921
|
+
email: createActivationEmail(),
|
|
886
922
|
activationOptions: {suppressEmail: true},
|
|
887
923
|
})
|
|
888
924
|
.then(() => {
|
|
@@ -896,7 +932,7 @@ describe('webex-core', () => {
|
|
|
896
932
|
|
|
897
933
|
return unauthServices
|
|
898
934
|
.validateUser({
|
|
899
|
-
email:
|
|
935
|
+
email: createActivationEmail(),
|
|
900
936
|
activationOptions: {suppressEmail: true},
|
|
901
937
|
preloginUserId,
|
|
902
938
|
})
|
|
@@ -913,7 +949,7 @@ describe('webex-core', () => {
|
|
|
913
949
|
|
|
914
950
|
return unauthServices
|
|
915
951
|
.validateUser({
|
|
916
|
-
email:
|
|
952
|
+
email: createActivationEmail(),
|
|
917
953
|
activationOptions: {suppressEmail: true},
|
|
918
954
|
})
|
|
919
955
|
.then(() => {
|
|
@@ -936,7 +972,7 @@ describe('webex-core', () => {
|
|
|
936
972
|
|
|
937
973
|
return userOnboardingServices
|
|
938
974
|
.validateUser({
|
|
939
|
-
email:
|
|
975
|
+
email: createActivationEmail(),
|
|
940
976
|
activationOptions: {suppressEmail: true},
|
|
941
977
|
})
|
|
942
978
|
.then(() => {
|
|
@@ -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();
|
|
@@ -112,6 +115,7 @@ describe('webex-core', () => {
|
|
|
112
115
|
services.initialize();
|
|
113
116
|
|
|
114
117
|
// call the onReady callback
|
|
118
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
115
119
|
services.listenToOnce.getCall(1).args[2]();
|
|
116
120
|
|
|
117
121
|
await waitForAsync();
|
|
@@ -122,6 +126,307 @@ describe('webex-core', () => {
|
|
|
122
126
|
`services: failed to init initial services when credentials available, ${expectedMessage}`
|
|
123
127
|
);
|
|
124
128
|
});
|
|
129
|
+
|
|
130
|
+
it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
|
|
131
|
+
services.listenToOnce = sinon.stub();
|
|
132
|
+
services.initialize();
|
|
133
|
+
|
|
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]();
|
|
137
|
+
|
|
138
|
+
assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
|
|
142
|
+
services.listenToOnce = sinon.stub();
|
|
143
|
+
services.initialize();
|
|
144
|
+
|
|
145
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
146
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
147
|
+
|
|
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
|
+
});
|
|
153
|
+
|
|
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
|
+
});
|
|
166
|
+
|
|
167
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
168
|
+
services.listenToOnce = sinon.stub();
|
|
169
|
+
services.initialize();
|
|
170
|
+
|
|
171
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
175
|
+
services.listenToOnce = sinon.stub();
|
|
176
|
+
services.initialize();
|
|
177
|
+
|
|
178
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
179
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
180
|
+
|
|
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
|
+
});
|
|
185
|
+
|
|
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
|
+
};
|
|
192
|
+
|
|
193
|
+
services.initialize();
|
|
194
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
195
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
196
|
+
await waitForAsync();
|
|
197
|
+
|
|
198
|
+
assert.isTrue(services.ready);
|
|
199
|
+
});
|
|
200
|
+
|
|
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());
|
|
204
|
+
|
|
205
|
+
services.initialize();
|
|
206
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
207
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
208
|
+
await waitForAsync();
|
|
209
|
+
|
|
210
|
+
assert.isTrue(services.ready);
|
|
211
|
+
});
|
|
212
|
+
|
|
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();
|
|
220
|
+
|
|
221
|
+
services.initialize();
|
|
222
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
223
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
224
|
+
await waitForAsync();
|
|
225
|
+
|
|
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);
|
|
429
|
+
});
|
|
125
430
|
});
|
|
126
431
|
|
|
127
432
|
describe('#initServiceCatalogs', () => {
|
|
@@ -1041,7 +1346,7 @@ describe('webex-core', () => {
|
|
|
1041
1346
|
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
1042
1347
|
// Cause ready callback to run immediately
|
|
1043
1348
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1044
|
-
if (event === 'ready') cb();
|
|
1349
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1045
1350
|
});
|
|
1046
1351
|
|
|
1047
1352
|
// Act
|
|
@@ -1083,7 +1388,7 @@ describe('webex-core', () => {
|
|
|
1083
1388
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1084
1389
|
// Cause ready callback to run immediately
|
|
1085
1390
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1086
|
-
if (event === 'ready') cb();
|
|
1391
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1087
1392
|
});
|
|
1088
1393
|
|
|
1089
1394
|
// Act
|