@webex/webex-core 3.12.0-next.41 → 3.12.0-next.42

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.
@@ -5,7 +5,7 @@
5
5
  import '@webex/internal-plugin-device';
6
6
 
7
7
  import {assert} from '@webex/test-helper-chai';
8
- import {flaky} from '@webex/test-helper-mocha';
8
+ import {flaky, retryOn429} from '@webex/test-helper-mocha';
9
9
  import WebexCore, {
10
10
  ServiceCatalogV2,
11
11
  ServiceDetail,
@@ -354,7 +354,10 @@ describe('webex-core', () => {
354
354
  // Before init settles, webex.ready must be false because services.ready
355
355
  // is a dependency and starts false in the gated path.
356
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');
357
+ assert.isFalse(
358
+ gatedWebex.ready,
359
+ 'webex.ready should not fire while services.ready is false'
360
+ );
358
361
 
359
362
  // Wait up to 30s for services init to complete and flip ready.
360
363
  await new Promise((resolve, reject) => {
@@ -374,7 +377,10 @@ describe('webex-core', () => {
374
377
  });
375
378
  });
376
379
 
377
- assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
380
+ assert.isTrue(
381
+ gatedWebex.internal.services.ready,
382
+ 'services.ready should flip true after init settles'
383
+ );
378
384
  assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
379
385
  });
380
386
  });
@@ -826,18 +832,22 @@ describe('webex-core', () => {
826
832
  }));
827
833
 
828
834
  it('validates an authorized user and webex instance', () =>
829
- services.validateUser({email: webexUser.email}).then((r) => {
830
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
831
- assert.equal(r.activated, true);
832
- assert.equal(r.exists, true);
833
- }));
835
+ retryOn429(() =>
836
+ services.validateUser({email: webexUser.email}).then((r) => {
837
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
838
+ assert.equal(r.activated, true);
839
+ assert.equal(r.exists, true);
840
+ })
841
+ ));
834
842
 
835
843
  it('validates an authorized EU user and webex instance', () =>
836
- servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
837
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
838
- assert.equal(r.activated, true);
839
- assert.equal(r.exists, true);
840
- }));
844
+ retryOn429(() =>
845
+ servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
846
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
847
+ assert.equal(r.activated, true);
848
+ assert.equal(r.exists, true);
849
+ })
850
+ ));
841
851
 
842
852
  it("returns a rejected promise if the provided email isn't valid", () =>
843
853
  unauthServices
@@ -850,113 +860,134 @@ describe('webex-core', () => {
850
860
  }));
851
861
 
852
862
  it('validates a non-existing user', () =>
853
- unauthServices
854
- .validateUser({email: createActivationEmail()})
855
- .then((r) => {
863
+ retryOn429(() =>
864
+ unauthServices.validateUser({email: createActivationEmail()}).then((r) => {
856
865
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
857
866
  assert.equal(r.activated, false);
858
867
  assert.equal(r.exists, false);
859
- }));
868
+ })
869
+ ));
860
870
 
861
871
  it('validates new user with activationOptions suppressEmail false', () =>
862
- unauthServices
863
- .validateUser({
864
- email: createActivationEmail(),
865
- activationOptions: {suppressEmail: false},
866
- })
867
- .then((r) => {
868
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
869
- assert.equal(r.activated, false);
870
- assert.equal(r.exists, false);
871
- assert.equal(r.user.verificationEmailTriggered, true);
872
- }));
872
+ retryOn429(() =>
873
+ unauthServices
874
+ .validateUser({
875
+ email: createActivationEmail(),
876
+ activationOptions: {suppressEmail: false},
877
+ })
878
+ .then((r) => {
879
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
880
+ assert.equal(r.activated, false);
881
+ assert.equal(r.exists, false);
882
+ assert.equal(r.user.verificationEmailTriggered, true);
883
+ })
884
+ ));
873
885
 
874
886
  it('validates new user with activationOptions suppressEmail true', () =>
875
- unauthServices
876
- .validateUser({
877
- email: createActivationEmail(),
878
- activationOptions: {suppressEmail: true},
879
- })
880
- .then((r) => {
881
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
882
- assert.equal(r.activated, false);
883
- assert.equal(r.exists, false);
884
- assert.equal(r.user.verificationEmailTriggered, false);
885
- }));
887
+ retryOn429(() =>
888
+ unauthServices
889
+ .validateUser({
890
+ email: createActivationEmail(),
891
+ activationOptions: {suppressEmail: true},
892
+ })
893
+ .then((r) => {
894
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
895
+ assert.equal(r.activated, false);
896
+ assert.equal(r.exists, false);
897
+ assert.equal(r.user.verificationEmailTriggered, false);
898
+ })
899
+ ));
886
900
 
887
901
  it('validates an inactive user', () => {
888
902
  const inactive = 'webex.web.client+nonactivated@gmail.com';
889
903
 
890
- return unauthServices
891
- .validateUser({email: inactive, activationOptions: {suppressEmail: true}})
892
- .then((r) => {
893
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
894
- assert.equal(r.activated, false, 'activated');
895
- assert.equal(r.exists, true, 'exists');
896
- })
897
- .catch(() => {
898
- assert(true);
899
- });
904
+ return retryOn429(() =>
905
+ unauthServices
906
+ .validateUser({email: inactive, activationOptions: {suppressEmail: true}})
907
+ .then((r) => {
908
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
909
+ assert.equal(r.activated, false, 'activated');
910
+ assert.equal(r.exists, true, 'exists');
911
+ })
912
+ ).catch(() => {
913
+ assert(true);
914
+ });
900
915
  });
901
916
 
902
917
  it('validates an existing user', () =>
903
- unauthServices.validateUser({email: webexUser.email}).then((r) => {
904
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
905
- assert.equal(r.activated, true);
906
- assert.equal(r.exists, true);
907
- }));
918
+ retryOn429(() =>
919
+ unauthServices.validateUser({email: webexUser.email}).then((r) => {
920
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
921
+ assert.equal(r.activated, true);
922
+ assert.equal(r.exists, true);
923
+ })
924
+ ));
908
925
 
909
926
  it('validates an existing EU user', () =>
910
- unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
911
- assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
912
- assert.equal(r.activated, true);
913
- assert.equal(r.exists, true);
914
- }));
927
+ retryOn429(() =>
928
+ unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
929
+ assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
930
+ assert.equal(r.activated, true);
931
+ assert.equal(r.exists, true);
932
+ })
933
+ ));
915
934
 
916
935
  it('sends the prelogin user id as undefined when not specified', () => {
917
936
  const requestStub = sandbox.spy(unauthServices, 'request');
918
937
 
919
- return unauthServices
920
- .validateUser({
921
- email: createActivationEmail(),
922
- activationOptions: {suppressEmail: true},
923
- })
924
- .then(() => {
925
- assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
926
- });
938
+ return retryOn429(() => {
939
+ requestStub.resetHistory();
940
+
941
+ return unauthServices
942
+ .validateUser({
943
+ email: createActivationEmail(),
944
+ activationOptions: {suppressEmail: true},
945
+ })
946
+ .then(() => {
947
+ assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
948
+ });
949
+ });
927
950
  });
928
951
 
929
952
  it('sends the prelogin user id as provided when specified', () => {
930
953
  const requestStub = sandbox.spy(unauthServices, 'request');
931
954
  const preloginUserId = uuid.v4();
932
955
 
933
- return unauthServices
934
- .validateUser({
935
- email: createActivationEmail(),
936
- activationOptions: {suppressEmail: true},
937
- preloginUserId,
938
- })
939
- .then(() => {
940
- assert.strictEqual(
941
- getActivationRequest(requestStub).headers['x-prelogin-userid'],
942
- preloginUserId
943
- );
944
- });
956
+ return retryOn429(() => {
957
+ requestStub.resetHistory();
958
+
959
+ return unauthServices
960
+ .validateUser({
961
+ email: createActivationEmail(),
962
+ activationOptions: {suppressEmail: true},
963
+ preloginUserId,
964
+ })
965
+ .then(() => {
966
+ assert.strictEqual(
967
+ getActivationRequest(requestStub).headers['x-prelogin-userid'],
968
+ preloginUserId
969
+ );
970
+ });
971
+ });
945
972
  });
946
973
 
947
974
  it('uses the license service by default', () => {
948
975
  const requestStub = sandbox.spy(unauthServices, 'request');
949
976
 
950
- return unauthServices
951
- .validateUser({
952
- email: createActivationEmail(),
953
- activationOptions: {suppressEmail: true},
954
- })
955
- .then(() => {
956
- const request = getActivationRequest(requestStub, false);
957
- assert.strictEqual(request.service, 'license');
958
- assert.strictEqual(request.resource, 'users/activations');
959
- });
977
+ return retryOn429(() => {
978
+ requestStub.resetHistory();
979
+
980
+ return unauthServices
981
+ .validateUser({
982
+ email: createActivationEmail(),
983
+ activationOptions: {suppressEmail: true},
984
+ })
985
+ .then(() => {
986
+ const request = getActivationRequest(requestStub, false);
987
+ assert.strictEqual(request.service, 'license');
988
+ assert.strictEqual(request.resource, 'users/activations');
989
+ });
990
+ });
960
991
  });
961
992
 
962
993
  it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
@@ -970,16 +1001,20 @@ describe('webex-core', () => {
970
1001
  const userOnboardingServices = userOnboardingWebex.internal.services;
971
1002
  const requestStub = sandbox.spy(userOnboardingServices, 'request');
972
1003
 
973
- return userOnboardingServices
974
- .validateUser({
975
- email: createActivationEmail(),
976
- activationOptions: {suppressEmail: true},
977
- })
978
- .then(() => {
979
- const request = getActivationRequest(requestStub, true);
980
- assert.strictEqual(request.service, 'user-onboarding');
981
- assert.strictEqual(request.resource, 'api/v1/users/activations');
982
- });
1004
+ return retryOn429(() => {
1005
+ requestStub.resetHistory();
1006
+
1007
+ return userOnboardingServices
1008
+ .validateUser({
1009
+ email: createActivationEmail(),
1010
+ activationOptions: {suppressEmail: true},
1011
+ })
1012
+ .then(() => {
1013
+ const request = getActivationRequest(requestStub, true);
1014
+ assert.strictEqual(request.service, 'user-onboarding');
1015
+ assert.strictEqual(request.resource, 'api/v1/users/activations');
1016
+ });
1017
+ });
983
1018
  });
984
1019
  });
985
1020
 
@@ -149,9 +149,55 @@ describe('webex-core', () => {
149
149
  const [, event] = services.listenToOnce.getCall(1).args;
150
150
  assert.equal(event, 'ready', 'default path listens on webex ready');
151
151
  });
152
+
153
+ it('skips preauth catalog collection when skipPreauthCatalogOnUnauthenticated is true and no credentials', async () => {
154
+ services.webex.config = {
155
+ ...(services.webex.config || {}),
156
+ services: {skipPreauthCatalogOnUnauthenticated: true},
157
+ };
158
+ services.listenToOnce = sinon.stub();
159
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
160
+ services.logger.info = sinon.stub();
161
+
162
+ services.initialize();
163
+ services.listenToOnce.getCall(0).args[2]();
164
+ services.listenToOnce.getCall(1).args[2]();
165
+ await waitForAsync();
166
+
167
+ assert.isFalse(
168
+ services.collectPreauthCatalog.called,
169
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
170
+ );
171
+ assert.isFalse(services.initFailed);
172
+ sinon.assert.calledWith(
173
+ services.logger.info,
174
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
175
+ );
176
+ });
177
+
178
+ it('still collects preauth catalog when skipPreauthCatalogOnUnauthenticated is false (default)', async () => {
179
+ services.webex.config = {
180
+ ...(services.webex.config || {}),
181
+ services: {skipPreauthCatalogOnUnauthenticated: false},
182
+ };
183
+ services.listenToOnce = sinon.stub();
184
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
185
+
186
+ services.initialize();
187
+ services.listenToOnce.getCall(0).args[2]();
188
+ services.listenToOnce.getCall(1).args[2]();
189
+ await waitForAsync();
190
+
191
+ assert.isTrue(
192
+ services.collectPreauthCatalog.called,
193
+ 'must collect preauth catalog when option is disabled'
194
+ );
195
+ });
152
196
  });
153
197
 
154
198
  describe('#initialize (waitForCatalogInit=true)', () => {
199
+ let clock;
200
+
155
201
  beforeEach(() => {
156
202
  services.webex.config = {
157
203
  ...(services.webex.config || {}),
@@ -162,6 +208,15 @@ describe('webex-core', () => {
162
208
  // true). We reset here so tests that call initialize() a second time
163
209
  // with the flag enabled observe the gated behavior from a clean state.
164
210
  services.ready = false;
211
+ // The gated path schedules a real 15s setTimeout via _makeInitTimeout()
212
+ // that is not cleared when the init race resolves. Fake only setTimeout/
213
+ // clearTimeout (leaving setImmediate real so waitForAsync still works)
214
+ // so restoring in afterEach clears the pending timer and Jest can exit.
215
+ clock = sinon.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
216
+ });
217
+
218
+ afterEach(() => {
219
+ clock.restore();
165
220
  });
166
221
 
167
222
  it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
@@ -242,7 +297,6 @@ describe('webex-core', () => {
242
297
  });
243
298
 
244
299
  it('flips services.ready=true when init times out', async () => {
245
- const clock = sinon.useFakeTimers();
246
300
  services.listenToOnce = sinon.stub();
247
301
  // Never resolves - the timeout must win the race.
248
302
  services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
@@ -259,7 +313,6 @@ describe('webex-core', () => {
259
313
  // rejected timeout can propagate through .catch and .finally into
260
314
  // _finalizeReady().
261
315
  await clock.tickAsync(15_001);
262
- clock.restore();
263
316
 
264
317
  assert.isTrue(services.initFailed);
265
318
  assert.isTrue(services.ready);
@@ -382,6 +435,86 @@ describe('webex-core', () => {
382
435
  });
383
436
  });
384
437
 
438
+ describe('#initialize (waitForCatalogInit=true, skipPreauthCatalogOnUnauthenticated=true)', () => {
439
+ beforeEach(() => {
440
+ services.webex.config = {
441
+ ...(services.webex.config || {}),
442
+ services: {
443
+ waitForCatalogInit: true,
444
+ catalogInitTimeout: 15000,
445
+ skipPreauthCatalogOnUnauthenticated: true,
446
+ },
447
+ };
448
+ services.ready = false;
449
+ });
450
+
451
+ it('skips preauth catalog collection but still finalizes services.ready when unauthenticated', async () => {
452
+ services.listenToOnce = sinon.stub();
453
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
454
+ services.logger.info = sinon.stub();
455
+
456
+ services.initialize();
457
+ services.listenToOnce.getCall(0).args[2]();
458
+ services.listenToOnce.getCall(1).args[2]();
459
+ await waitForAsync();
460
+
461
+ assert.isFalse(
462
+ services.collectPreauthCatalog.called,
463
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
464
+ );
465
+ assert.isTrue(services.ready, 'must still finalize ready so webex.ready is not stalled');
466
+ sinon.assert.calledWith(
467
+ services.logger.info,
468
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
469
+ );
470
+ });
471
+
472
+ it('still registers a change:canAuthorize listener when skipping preauth (fresh-login path)', async () => {
473
+ services.listenToOnce = sinon.stub();
474
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
475
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
476
+
477
+ services.initialize();
478
+ const currentCatalog = services._getCatalog();
479
+ services.listenToOnce.getCall(0).args[2]();
480
+ services.listenToOnce.getCall(1).args[2]();
481
+ await waitForAsync();
482
+
483
+ const freshLoginCall = services.listenToOnce
484
+ .getCalls()
485
+ .find((call) => call.args[1] === 'change:canAuthorize');
486
+ assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
487
+
488
+ services.webex.canAuthorize = true;
489
+ currentCatalog.status.postauth.ready = false;
490
+ freshLoginCall.args[2]();
491
+
492
+ assert.isTrue(
493
+ services.initServiceCatalogs.called,
494
+ 'expected postauth catalog init after canAuthorize flips'
495
+ );
496
+ await waitForAsync();
497
+ });
498
+
499
+ it('does not schedule an init timeout when skipping preauth (no stray timer/rejection)', async () => {
500
+ const clock = sinon.useFakeTimers();
501
+ services.listenToOnce = sinon.stub();
502
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
503
+ services.logger.error = sinon.stub();
504
+
505
+ services.initialize();
506
+ services.listenToOnce.getCall(0).args[2]();
507
+ services.listenToOnce.getCall(1).args[2]();
508
+
509
+ // Advance well past the configured init timeout; since no timeout was
510
+ // scheduled in skip mode, no timeout error should ever be logged.
511
+ await clock.tickAsync(30_000);
512
+ clock.restore();
513
+
514
+ sinon.assert.neverCalledWithMatch(services.logger.error, /services: init timed out/);
515
+ });
516
+ });
517
+
385
518
  describe('#_finalizeReady', () => {
386
519
  beforeEach(() => {
387
520
  // Outer beforeEach construction already ran initialize() (ungated,
@@ -151,9 +151,55 @@ describe('webex-core', () => {
151
151
  const [, event] = services.listenToOnce.getCall(1).args;
152
152
  assert.equal(event, 'ready', 'default path listens on webex ready');
153
153
  });
154
+
155
+ it('skips preauth catalog collection when skipPreauthCatalogOnUnauthenticated is true and no credentials', async () => {
156
+ services.webex.config = {
157
+ ...(services.webex.config || {}),
158
+ services: {skipPreauthCatalogOnUnauthenticated: true},
159
+ };
160
+ services.listenToOnce = sinon.stub();
161
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
162
+ services.logger.info = sinon.stub();
163
+
164
+ services.initialize();
165
+ services.listenToOnce.getCall(0).args[2]();
166
+ services.listenToOnce.getCall(1).args[2]();
167
+ await waitForAsync();
168
+
169
+ assert.isFalse(
170
+ services.collectPreauthCatalog.called,
171
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
172
+ );
173
+ assert.isFalse(services.initFailed);
174
+ sinon.assert.calledWith(
175
+ services.logger.info,
176
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
177
+ );
178
+ });
179
+
180
+ it('still collects preauth catalog when skipPreauthCatalogOnUnauthenticated is false (default)', async () => {
181
+ services.webex.config = {
182
+ ...(services.webex.config || {}),
183
+ services: {skipPreauthCatalogOnUnauthenticated: false},
184
+ };
185
+ services.listenToOnce = sinon.stub();
186
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
187
+
188
+ services.initialize();
189
+ services.listenToOnce.getCall(0).args[2]();
190
+ services.listenToOnce.getCall(1).args[2]();
191
+ await waitForAsync();
192
+
193
+ assert.isTrue(
194
+ services.collectPreauthCatalog.called,
195
+ 'must collect preauth catalog when option is disabled'
196
+ );
197
+ });
154
198
  });
155
199
 
156
200
  describe('#initialize (waitForCatalogInit=true)', () => {
201
+ let clock;
202
+
157
203
  beforeEach(() => {
158
204
  services.webex.config = {
159
205
  ...(services.webex.config || {}),
@@ -164,6 +210,15 @@ describe('webex-core', () => {
164
210
  // true). We reset here so tests that call initialize() a second time
165
211
  // with the flag enabled observe the gated behavior from a clean state.
166
212
  services.ready = false;
213
+ // The gated path schedules a real 15s setTimeout via _makeInitTimeout()
214
+ // that is not cleared when the init race resolves. Fake only setTimeout/
215
+ // clearTimeout (leaving setImmediate real so waitForAsync still works)
216
+ // so restoring in afterEach clears the pending timer and Jest can exit.
217
+ clock = sinon.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
218
+ });
219
+
220
+ afterEach(() => {
221
+ clock.restore();
167
222
  });
168
223
 
169
224
  it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
@@ -243,7 +298,6 @@ describe('webex-core', () => {
243
298
  });
244
299
 
245
300
  it('flips services.ready=true when init times out', async () => {
246
- const clock = sinon.useFakeTimers();
247
301
  services.listenToOnce = sinon.stub();
248
302
  services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
249
303
  services.webex.credentials = {
@@ -256,7 +310,6 @@ describe('webex-core', () => {
256
310
  services.listenToOnce.getCall(1).args[2]();
257
311
 
258
312
  await clock.tickAsync(15_001);
259
- clock.restore();
260
313
 
261
314
  assert.isTrue(services.initFailed);
262
315
  assert.isTrue(services.ready);
@@ -376,6 +429,89 @@ describe('webex-core', () => {
376
429
  });
377
430
  });
378
431
 
432
+ describe('#initialize (waitForCatalogInit=true, skipPreauthCatalogOnUnauthenticated=true)', () => {
433
+ beforeEach(() => {
434
+ services.webex.config = {
435
+ ...(services.webex.config || {}),
436
+ services: {
437
+ waitForCatalogInit: true,
438
+ catalogInitTimeout: 15000,
439
+ skipPreauthCatalogOnUnauthenticated: true,
440
+ },
441
+ };
442
+ services.ready = false;
443
+ });
444
+
445
+ it('skips preauth catalog collection but still finalizes services.ready when unauthenticated', async () => {
446
+ services.listenToOnce = sinon.stub();
447
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
448
+ services.logger.info = sinon.stub();
449
+
450
+ services.initialize();
451
+ services.listenToOnce.getCall(0).args[2]();
452
+ services.listenToOnce.getCall(1).args[2]();
453
+ await waitForAsync();
454
+
455
+ assert.isFalse(
456
+ services.collectPreauthCatalog.called,
457
+ 'must not collect preauth catalog while unauthenticated when option is enabled'
458
+ );
459
+ assert.isTrue(services.ready, 'must still finalize ready so webex.ready is not stalled');
460
+ sinon.assert.calledWith(
461
+ services.logger.info,
462
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
463
+ );
464
+ });
465
+
466
+ it('still registers a change:canAuthorize listener when skipping preauth (fresh-login path)', async () => {
467
+ services.listenToOnce = sinon.stub();
468
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
469
+ services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
470
+
471
+ services.initialize();
472
+ const currentCatalog = services._getCatalog();
473
+ services.listenToOnce.getCall(0).args[2]();
474
+ services.listenToOnce.getCall(1).args[2]();
475
+ await waitForAsync();
476
+
477
+ const freshLoginCall = services.listenToOnce
478
+ .getCalls()
479
+ .find((call: any) => call.args[1] === 'change:canAuthorize');
480
+ assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
481
+
482
+ services.webex.canAuthorize = true;
483
+ currentCatalog.status.postauth.ready = false;
484
+ freshLoginCall!.args[2]();
485
+
486
+ assert.isTrue(
487
+ services.initServiceCatalogs.called,
488
+ 'expected postauth catalog init after canAuthorize flips'
489
+ );
490
+ await waitForAsync();
491
+ });
492
+
493
+ it('does not schedule an init timeout when skipping preauth (no stray timer/rejection)', async () => {
494
+ const clock = sinon.useFakeTimers();
495
+ services.listenToOnce = sinon.stub();
496
+ services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
497
+ services.logger.error = sinon.stub();
498
+
499
+ services.initialize();
500
+ services.listenToOnce.getCall(0).args[2]();
501
+ services.listenToOnce.getCall(1).args[2]();
502
+
503
+ // Advance well past the configured init timeout; since no timeout was
504
+ // scheduled in skip mode, no timeout error should ever be logged.
505
+ await clock.tickAsync(30_000);
506
+ clock.restore();
507
+
508
+ sinon.assert.neverCalledWithMatch(
509
+ services.logger.error,
510
+ /services: init timed out/
511
+ );
512
+ });
513
+ });
514
+
379
515
  describe('#_finalizeReady', () => {
380
516
  beforeEach(() => {
381
517
  // Outer beforeEach construction already ran initialize() (ungated,