@webex/webex-core 3.12.0-next.32 → 3.12.0-next.34

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.
@@ -57,6 +57,22 @@ const Services = WebexPlugin.extend({
57
57
  initFailed: ['boolean', false, false],
58
58
  },
59
59
 
60
+ session: {
61
+ /**
62
+ * Becomes `true` once the initial catalog collection has completed
63
+ * (successfully or otherwise) and any in-flight credentials refresh has
64
+ * settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
65
+ * implying "catalogs populated AND credential state stable".
66
+ * @instance
67
+ * @memberof Services
68
+ * @type {boolean}
69
+ */
70
+ ready: {
71
+ default: false,
72
+ type: 'boolean',
73
+ },
74
+ },
75
+
60
76
  _catalogs: new WeakMap(),
61
77
 
62
78
  _serviceUrls: null,
@@ -1346,6 +1362,7 @@ const Services = WebexPlugin.extend({
1346
1362
 
1347
1363
  // Destructure the credentials plugin.
1348
1364
  const {credentials} = this.webex;
1365
+ const catalog = this._getCatalog();
1349
1366
 
1350
1367
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1351
1368
  // credentials#getOrgId() to properly throw.
@@ -1358,12 +1375,18 @@ const Services = WebexPlugin.extend({
1358
1375
  .then(() => {
1359
1376
  // Validate if the token is authorized.
1360
1377
  if (credentials.canAuthorize) {
1361
- // Attempt to collect the postauth catalog.
1362
-
1363
- return this.updateServices().catch(() => {
1364
- this.initFailed = true;
1365
- this.logger.warn('services: cannot retrieve postauth catalog');
1366
- });
1378
+ // Attempt to collect the postauth catalog, then mark the catalog
1379
+ // ready. Setting `isReady` here - rather than only in the init
1380
+ // callers - means a slow postauth fetch that loses the gated-init
1381
+ // timeout race still marks the catalog ready once it completes.
1382
+ return this.updateServices()
1383
+ .then(() => {
1384
+ catalog.isReady = true;
1385
+ })
1386
+ .catch(() => {
1387
+ this.initFailed = true;
1388
+ this.logger.warn('services: cannot retrieve postauth catalog');
1389
+ });
1367
1390
  }
1368
1391
 
1369
1392
  // Return a resolved promise for consistent return value.
@@ -1372,6 +1395,29 @@ const Services = WebexPlugin.extend({
1372
1395
  );
1373
1396
  },
1374
1397
 
1398
+ /**
1399
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1400
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1401
+ * refresh is in flight when initial catalog collection settles, we must not
1402
+ * signal ready until the refresh has resolved - otherwise downstream
1403
+ * consumers may observe `canAuthorize`/token state that is about to change
1404
+ * under them.
1405
+ *
1406
+ * @private
1407
+ * @returns {Promise<void>}
1408
+ */
1409
+ async _finalizeReady() {
1410
+ const {credentials} = this.webex;
1411
+
1412
+ if (credentials && credentials.isRefreshing) {
1413
+ await new Promise((resolve) => {
1414
+ credentials.once('change:isRefreshing', resolve);
1415
+ });
1416
+ }
1417
+
1418
+ this.ready = true;
1419
+ },
1420
+
1375
1421
  /**
1376
1422
  * Initializer
1377
1423
  *
@@ -1388,11 +1434,39 @@ const Services = WebexPlugin.extend({
1388
1434
  this.registries.set(this.webex, registry);
1389
1435
  this.states.set(this.webex, state);
1390
1436
 
1391
- // Listen for configuration changes once.
1437
+ // Listen for configuration changes once. The config is not populated on the
1438
+ // webex instance until the `change:config` event fires, so any decision that
1439
+ // depends on config values (such as the gated-vs-ungated init below) must be
1440
+ // made from within this handler rather than synchronously in `initialize()`.
1392
1441
  this.listenToOnce(this.webex, 'change:config', () => {
1393
1442
  this.initConfig();
1443
+
1444
+ // Feature flag: when enabled, `webex.ready` is blocked until the initial
1445
+ // catalog collection has settled AND any in-flight credentials refresh has
1446
+ // completed. When disabled (the default), preserves the pre-existing
1447
+ // behavior where `webex.ready` fires as soon as `webex.loaded` does and
1448
+ // the catalog is collected out-of-band.
1449
+ const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
1450
+
1451
+ if (waitForCatalogInit) {
1452
+ this._initializeCatalogsGated(catalog);
1453
+ } else {
1454
+ // Not gating - immediately mark ready so we do not block webex.ready.
1455
+ this.ready = true;
1456
+ this._initializeCatalogsUngated(catalog);
1457
+ }
1394
1458
  });
1459
+ },
1395
1460
 
1461
+ /**
1462
+ * Original (pre-verified-ready) initialization path. Runs on `webex.ready`
1463
+ * and collects catalogs opportunistically without blocking anything.
1464
+ *
1465
+ * @private
1466
+ * @param {ServiceCatalog} catalog
1467
+ * @returns {void}
1468
+ */
1469
+ _initializeCatalogsUngated(catalog) {
1396
1470
  // wait for webex instance to be ready before attempting
1397
1471
  // to update the service catalogs
1398
1472
  // this can cause a race condition because credentials may
@@ -1407,16 +1481,14 @@ const Services = WebexPlugin.extend({
1407
1481
  const {supertoken} = this.webex.credentials;
1408
1482
  // Validate if the supertoken exists.
1409
1483
  if (supertoken && supertoken.access_token) {
1410
- this.initServiceCatalogs()
1411
- .then(() => {
1412
- catalog.isReady = true;
1413
- })
1414
- .catch((error) => {
1415
- this.initFailed = true;
1416
- this.logger.error(
1417
- `services: failed to init initial services when credentials available, ${error?.message}`
1418
- );
1419
- });
1484
+ // `initServiceCatalogs` marks the catalog ready internally once the
1485
+ // postauth catalog is collected.
1486
+ this.initServiceCatalogs().catch((error) => {
1487
+ this.initFailed = true;
1488
+ this.logger.error(
1489
+ `services: failed to init initial services when credentials available, ${error?.message}`
1490
+ );
1491
+ });
1420
1492
  } else {
1421
1493
  const {email} = this.webex.config;
1422
1494
 
@@ -1429,6 +1501,87 @@ const Services = WebexPlugin.extend({
1429
1501
  }
1430
1502
  });
1431
1503
  },
1504
+
1505
+ /**
1506
+ * Verified-ready initialization path. Blocks `webex.ready` until the initial
1507
+ * catalog fetch has settled (or timed out) AND any in-flight credentials
1508
+ * refresh has completed. Also handles the fresh-login case where OAuth
1509
+ * completes after `loaded` fires.
1510
+ *
1511
+ * @private
1512
+ * @param {ServiceCatalog} catalog
1513
+ * @returns {void}
1514
+ */
1515
+ _initializeCatalogsGated(catalog) {
1516
+ // Wait for storage to be loaded before attempting to update the service
1517
+ // catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
1518
+ // now blocks `webex.ready` - listening to 'ready' would deadlock.
1519
+ this.listenToOnce(this.webex, 'loaded', async () => {
1520
+ const cachedCatalog = await this._loadCatalogFromCache();
1521
+ if (cachedCatalog) {
1522
+ catalog.isReady = true;
1523
+ await this._finalizeReady();
1524
+
1525
+ return; // skip initServiceCatalogs() on reload when cache exists
1526
+ }
1527
+ const {supertoken} = this.webex.credentials;
1528
+
1529
+ // Race init against a hard timeout so a hung request never leaves
1530
+ // `services.ready` false forever - that would stall `webex.ready` and
1531
+ // leave consumers waiting on it indefinitely. Timeout is configurable via
1532
+ // `config.services.catalogInitTimeout` (defaults to 15s in config).
1533
+ const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
1534
+
1535
+ const initServiceCatalogsTimeout = new Promise((_, reject) => {
1536
+ setTimeout(
1537
+ () => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
1538
+ initTimeoutMs
1539
+ );
1540
+ });
1541
+
1542
+ // Validate if the supertoken exists.
1543
+ if (supertoken && supertoken.access_token) {
1544
+ // `initServiceCatalogs` marks the catalog ready internally once the
1545
+ // postauth catalog is collected - even if it loses the timeout race
1546
+ // above, so a slow fetch still eventually flips `catalog.isReady`.
1547
+ Promise.race([this.initServiceCatalogs(), initServiceCatalogsTimeout])
1548
+ .catch((error) => {
1549
+ this.initFailed = true;
1550
+ this.logger.error(
1551
+ `services: failed to init initial services when credentials available, ${error?.message}`
1552
+ );
1553
+ })
1554
+ .finally(() => this._finalizeReady());
1555
+ } else {
1556
+ const {email} = this.webex.config;
1557
+
1558
+ Promise.race([
1559
+ this.collectPreauthCatalog(email ? {email} : undefined),
1560
+ initServiceCatalogsTimeout,
1561
+ ])
1562
+ .catch((error) => {
1563
+ this.initFailed = true;
1564
+ this.logger.error(
1565
+ `services: failed to init initial services when no credentials available, ${error?.message}`
1566
+ );
1567
+ })
1568
+ .finally(() => this._finalizeReady());
1569
+
1570
+ // Handle fresh login: 'loaded' fires before OAuth completes, so listen
1571
+ // for `canAuthorize` flipping true and then collect the postauth catalog.
1572
+ this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1573
+ if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1574
+ // `initServiceCatalogs` marks the catalog ready internally.
1575
+ this.initServiceCatalogs().catch((error) => {
1576
+ this.logger.error(
1577
+ `services: failed to init service catalogs after auth, ${error?.message}`
1578
+ );
1579
+ });
1580
+ }
1581
+ });
1582
+ }
1583
+ });
1584
+ },
1432
1585
  });
1433
1586
  /* eslint-enable no-underscore-dangle */
1434
1587
 
@@ -44,6 +44,22 @@ const Services = WebexPlugin.extend({
44
44
  initFailed: ['boolean', false, false],
45
45
  },
46
46
 
47
+ session: {
48
+ /**
49
+ * Becomes `true` once the initial catalog collection has completed
50
+ * (successfully or otherwise) and any in-flight credentials refresh has
51
+ * settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
52
+ * implying "catalogs populated AND credential state stable".
53
+ * @instance
54
+ * @memberof Services
55
+ * @type {boolean}
56
+ */
57
+ ready: {
58
+ default: false,
59
+ type: 'boolean',
60
+ },
61
+ },
62
+
47
63
  _catalogs: new WeakMap(),
48
64
 
49
65
  _activeServices: {},
@@ -1312,6 +1328,7 @@ const Services = WebexPlugin.extend({
1312
1328
 
1313
1329
  // Destructure the credentials plugin.
1314
1330
  const {credentials} = this.webex;
1331
+ const catalog = this._getCatalog();
1315
1332
 
1316
1333
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1317
1334
  // credentials#getOrgId() to properly throw.
@@ -1324,11 +1341,18 @@ const Services = WebexPlugin.extend({
1324
1341
  .then(() => {
1325
1342
  // Validate if the token is authorized.
1326
1343
  if (credentials.canAuthorize) {
1327
- // Attempt to collect the postauth catalog.
1328
- return this.updateServices({forceRefresh: refresh}).catch(() => {
1329
- this.initFailed = true;
1330
- this.logger.warn('services: cannot retrieve postauth catalog');
1331
- });
1344
+ // Attempt to collect the postauth catalog, then mark the catalog
1345
+ // ready. Setting `isReady` here - rather than only in the init
1346
+ // callers - means a slow postauth fetch that loses the gated-init
1347
+ // timeout race still marks the catalog ready once it completes.
1348
+ return this.updateServices({forceRefresh: refresh})
1349
+ .then(() => {
1350
+ catalog.isReady = true;
1351
+ })
1352
+ .catch(() => {
1353
+ this.initFailed = true;
1354
+ this.logger.warn('services: cannot retrieve postauth catalog');
1355
+ });
1332
1356
  }
1333
1357
 
1334
1358
  // Return a resolved promise for consistent return value.
@@ -1337,6 +1361,29 @@ const Services = WebexPlugin.extend({
1337
1361
  );
1338
1362
  },
1339
1363
 
1364
+ /**
1365
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1366
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1367
+ * refresh is in flight when initial catalog collection settles, we must not
1368
+ * signal ready until the refresh has resolved - otherwise downstream
1369
+ * consumers may observe `canAuthorize`/token state that is about to change
1370
+ * under them.
1371
+ *
1372
+ * @private
1373
+ * @returns {Promise<void>}
1374
+ */
1375
+ async _finalizeReady(): Promise<void> {
1376
+ const {credentials} = this.webex;
1377
+
1378
+ if (credentials && credentials.isRefreshing) {
1379
+ await new Promise<void>((resolve) => {
1380
+ credentials.once('change:isRefreshing', resolve);
1381
+ });
1382
+ }
1383
+
1384
+ this.ready = true;
1385
+ },
1386
+
1340
1387
  /**
1341
1388
  * Initializer
1342
1389
  *
@@ -1348,11 +1395,39 @@ const Services = WebexPlugin.extend({
1348
1395
  const catalog = new ServiceCatalog();
1349
1396
  this._catalogs.set(this.webex, catalog);
1350
1397
 
1351
- // Listen for configuration changes once.
1398
+ // Listen for configuration changes once. The config is not populated on the
1399
+ // webex instance until the `change:config` event fires, so any decision that
1400
+ // depends on config values (such as the gated-vs-ungated init below) must be
1401
+ // made from within this handler rather than synchronously in `initialize()`.
1352
1402
  this.listenToOnce(this.webex, 'change:config', () => {
1353
1403
  this.initConfig();
1404
+
1405
+ // Feature flag: when enabled, `webex.ready` is blocked until the initial
1406
+ // catalog collection has settled AND any in-flight credentials refresh has
1407
+ // completed. When disabled (the default), preserves the pre-existing
1408
+ // behavior where `webex.ready` fires as soon as `webex.loaded` does and
1409
+ // the catalog is collected out-of-band.
1410
+ const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
1411
+
1412
+ if (waitForCatalogInit) {
1413
+ this._initializeCatalogsGated(catalog);
1414
+ } else {
1415
+ // Not gating - immediately mark ready so we do not block webex.ready.
1416
+ this.ready = true;
1417
+ this._initializeCatalogsUngated(catalog);
1418
+ }
1354
1419
  });
1420
+ },
1355
1421
 
1422
+ /**
1423
+ * Original (pre-verified-ready) initialization path. Runs on `webex.ready`
1424
+ * and collects catalogs opportunistically without blocking anything.
1425
+ *
1426
+ * @private
1427
+ * @param {ServiceCatalog} catalog
1428
+ * @returns {void}
1429
+ */
1430
+ _initializeCatalogsUngated(catalog: ServiceCatalog): void {
1356
1431
  // wait for webex instance to be ready before attempting
1357
1432
  // to update the service catalogs
1358
1433
  this.listenToOnce(this.webex, 'ready', async () => {
@@ -1365,16 +1440,14 @@ const Services = WebexPlugin.extend({
1365
1440
  const {supertoken} = this.webex.credentials;
1366
1441
  // Validate if the supertoken exists.
1367
1442
  if (supertoken && supertoken.access_token) {
1368
- this.initServiceCatalogs()
1369
- .then(() => {
1370
- catalog.isReady = true;
1371
- })
1372
- .catch((error) => {
1373
- this.initFailed = true;
1374
- this.logger.error(
1375
- `services: failed to init initial services when credentials available, ${error?.message}`
1376
- );
1377
- });
1443
+ // `initServiceCatalogs` marks the catalog ready internally once the
1444
+ // postauth catalog is collected.
1445
+ this.initServiceCatalogs().catch((error) => {
1446
+ this.initFailed = true;
1447
+ this.logger.error(
1448
+ `services: failed to init initial services when credentials available, ${error?.message}`
1449
+ );
1450
+ });
1378
1451
  } else {
1379
1452
  const {email} = this.webex.config;
1380
1453
 
@@ -1387,6 +1460,86 @@ const Services = WebexPlugin.extend({
1387
1460
  }
1388
1461
  });
1389
1462
  },
1463
+
1464
+ /**
1465
+ * Verified-ready initialization path. Blocks `webex.ready` until the initial
1466
+ * catalog fetch has settled (or timed out) AND any in-flight credentials
1467
+ * refresh has completed. Also handles the fresh-login case where OAuth
1468
+ * completes after `loaded` fires.
1469
+ *
1470
+ * @private
1471
+ * @param {ServiceCatalog} catalog
1472
+ * @returns {void}
1473
+ */
1474
+ _initializeCatalogsGated(catalog: ServiceCatalog): void {
1475
+ // Wait for storage to be loaded before attempting to update the service
1476
+ // catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
1477
+ // now blocks `webex.ready` - listening to 'ready' would deadlock.
1478
+ this.listenToOnce(this.webex, 'loaded', async () => {
1479
+ const warmed = await this._loadCatalogFromCache();
1480
+ if (warmed) {
1481
+ catalog.isReady = true;
1482
+ await this._finalizeReady();
1483
+
1484
+ return;
1485
+ }
1486
+ const {supertoken} = this.webex.credentials;
1487
+
1488
+ // Race init against a hard timeout so a hung request never leaves
1489
+ // `services.ready` false forever - that would stall `webex.ready` and
1490
+ // leave consumers waiting on it indefinitely. Timeout is configurable via
1491
+ // `config.services.catalogInitTimeout` (defaults to 15s in config).
1492
+ const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
1493
+ const initServiceCatalogsTimeout = new Promise<never>((_, reject) => {
1494
+ setTimeout(
1495
+ () => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
1496
+ initTimeoutMs
1497
+ );
1498
+ });
1499
+
1500
+ // Validate if the supertoken exists.
1501
+ if (supertoken && supertoken.access_token) {
1502
+ // `initServiceCatalogs` marks the catalog ready internally once the
1503
+ // postauth catalog is collected - even if it loses the timeout race
1504
+ // above, so a slow fetch still eventually flips `catalog.isReady`.
1505
+ Promise.race([this.initServiceCatalogs(), initServiceCatalogsTimeout])
1506
+ .catch((error) => {
1507
+ this.initFailed = true;
1508
+ this.logger.error(
1509
+ `services: failed to init initial services when credentials available, ${error?.message}`
1510
+ );
1511
+ })
1512
+ .finally(() => this._finalizeReady());
1513
+ } else {
1514
+ const {email} = this.webex.config;
1515
+
1516
+ Promise.race([
1517
+ this.collectPreauthCatalog(email ? {email} : undefined),
1518
+ initServiceCatalogsTimeout,
1519
+ ])
1520
+ .catch((error) => {
1521
+ this.initFailed = true;
1522
+ this.logger.error(
1523
+ `services: failed to init initial services when no credentials available, ${error?.message}`
1524
+ );
1525
+ })
1526
+ .finally(() => this._finalizeReady());
1527
+
1528
+ // Handle fresh login: 'loaded' fires before OAuth completes, so listen
1529
+ // for `canAuthorize` flipping true and then collect the postauth catalog.
1530
+ this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1531
+ if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1532
+ // `initServiceCatalogs` marks the catalog ready internally.
1533
+ this.initServiceCatalogs().catch((error) => {
1534
+ this.logger.error(
1535
+ `services: failed to init service catalogs after auth, ${error?.message}`
1536
+ );
1537
+ });
1538
+ }
1539
+ });
1540
+ }
1541
+ });
1542
+ },
1390
1543
  });
1391
1544
  /* eslint-enable no-underscore-dangle */
1392
1545
 
@@ -0,0 +1,22 @@
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import uuid from 'uuid';
6
+
7
+ /**
8
+ * Generates a unique test email for user-activation/validation specs.
9
+ *
10
+ * The local part is kept short on purpose: for a brand-new self-signup user the
11
+ * backend derives the "given name" from the email local part, and self-signup
12
+ * orgs cap the given name at 50 characters. A full UUID would push the local
13
+ * part to 59 chars and fail with errorCode 100018, so we use 20 hex characters
14
+ * of entropy (local part = 43 chars).
15
+ *
16
+ * @returns {string} e.g. `Collabctg+webex-js-sdk-1a2b3c4d5e6f7a8b9c0d@gmail.com`
17
+ */
18
+ export function createActivationEmail(): string {
19
+ return `Collabctg+webex-js-sdk-${uuid.v4().replace(/-/g, '').slice(0, 20)}@gmail.com`;
20
+ }
21
+
22
+ export default createActivationEmail;
@@ -20,6 +20,7 @@ import WebexCore, {
20
20
  import testUsers from '@webex/test-helper-test-users';
21
21
  import uuid from 'uuid';
22
22
  import sinon from 'sinon';
23
+ import {createActivationEmail} from '../../../fixtures/activation-email';
23
24
 
24
25
  /* eslint-disable no-underscore-dangle */
25
26
  describe('webex-core', () => {
@@ -408,11 +409,13 @@ describe('webex-core', () => {
408
409
  services._loadCatalogFromCache = sinon.stub().resolves(false);
409
410
  services.initServiceCatalogs = sinon.stub().resolves();
410
411
  services.initialize();
412
+ // The mode-specific ('ready'/'loaded') listener is registered inside the
413
+ // change:config handler, so fire change:config first, then 'ready'.
414
+ webex.trigger('change:config');
411
415
  webex.trigger('ready');
412
416
  // Wait for the async 'ready' handler to complete
413
417
  await new Promise((resolve) => setTimeout(resolve, 50));
414
418
  assert.called(services.initServiceCatalogs);
415
- assert.isTrue(catalog.isReady);
416
419
  });
417
420
 
418
421
  it('should collect different catalogs based on OrgId region', () =>
@@ -430,6 +433,39 @@ describe('webex-core', () => {
430
433
  done();
431
434
  }, 2000);
432
435
  });
436
+
437
+ it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
438
+ const gatedWebex = new WebexCore({
439
+ credentials: {supertoken: webexUser.token},
440
+ config: {services: {waitForCatalogInit: true}},
441
+ });
442
+
443
+ // Before init settles, webex.ready must be false because services.ready
444
+ // is a dependency and starts false in the gated path.
445
+ assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
446
+ assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
447
+
448
+ // Wait up to 30s for services init to complete and flip ready.
449
+ await new Promise((resolve, reject) => {
450
+ if (gatedWebex.internal.services.ready) {
451
+ resolve();
452
+
453
+ return;
454
+ }
455
+ const timer = setTimeout(
456
+ () => reject(new Error('timed out waiting for services.ready')),
457
+ 30_000
458
+ );
459
+
460
+ gatedWebex.internal.services.once('change:ready', () => {
461
+ clearTimeout(timer);
462
+ resolve();
463
+ });
464
+ });
465
+
466
+ assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
467
+ assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
468
+ });
433
469
  });
434
470
 
435
471
  describe('#initServiceCatalogs()', () => {
@@ -892,7 +928,7 @@ describe('webex-core', () => {
892
928
 
893
929
  it('validates a non-existing user', () =>
894
930
  unauthServices
895
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
931
+ .validateUser({email: createActivationEmail()})
896
932
  .then((r) => {
897
933
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
898
934
  assert.equal(r.activated, false);
@@ -905,7 +941,7 @@ describe('webex-core', () => {
905
941
  it('validates new user with activationOptions suppressEmail false', () =>
906
942
  unauthServices
907
943
  .validateUser({
908
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
944
+ email: createActivationEmail(),
909
945
  activationOptions: {suppressEmail: false},
910
946
  })
911
947
  .then((r) => {
@@ -921,7 +957,7 @@ describe('webex-core', () => {
921
957
  it('validates new user with activationOptions suppressEmail true', () =>
922
958
  unauthServices
923
959
  .validateUser({
924
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
960
+ email: createActivationEmail(),
925
961
  activationOptions: {suppressEmail: true},
926
962
  })
927
963
  .then((r) => {
@@ -977,7 +1013,7 @@ describe('webex-core', () => {
977
1013
 
978
1014
  return unauthServices
979
1015
  .validateUser({
980
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1016
+ email: createActivationEmail(),
981
1017
  activationOptions: {suppressEmail: true},
982
1018
  })
983
1019
  .then(() => {
@@ -991,7 +1027,7 @@ describe('webex-core', () => {
991
1027
 
992
1028
  return unauthServices
993
1029
  .validateUser({
994
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1030
+ email: createActivationEmail(),
995
1031
  activationOptions: {suppressEmail: true},
996
1032
  preloginUserId,
997
1033
  })
@@ -1008,7 +1044,7 @@ describe('webex-core', () => {
1008
1044
 
1009
1045
  return unauthServices
1010
1046
  .validateUser({
1011
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1047
+ email: createActivationEmail(),
1012
1048
  activationOptions: {suppressEmail: true},
1013
1049
  })
1014
1050
  .then(() => {
@@ -1031,7 +1067,7 @@ describe('webex-core', () => {
1031
1067
 
1032
1068
  return userOnboardingServices
1033
1069
  .validateUser({
1034
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1070
+ email: createActivationEmail(),
1035
1071
  activationOptions: {suppressEmail: true},
1036
1072
  })
1037
1073
  .then(() => {