@webex/webex-core 3.12.0-webex-services-ready.2 → 3.12.0-webex-services-ready.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/lib/batcher.js +1 -1
  5. package/dist/lib/credentials/credentials.js +1 -1
  6. package/dist/lib/credentials/token.js +1 -1
  7. package/dist/lib/domains.js +90 -0
  8. package/dist/lib/domains.js.map +1 -0
  9. package/dist/lib/services/service-catalog.js +6 -10
  10. package/dist/lib/services/service-catalog.js.map +1 -1
  11. package/dist/lib/services/services.js +26 -22
  12. package/dist/lib/services/services.js.map +1 -1
  13. package/dist/lib/services-v2/service-catalog.js +6 -12
  14. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  15. package/dist/lib/services-v2/services-v2.js +33 -24
  16. package/dist/lib/services-v2/services-v2.js.map +1 -1
  17. package/dist/plugins/logger.js +1 -1
  18. package/dist/webex-core.js +2 -2
  19. package/package.json +13 -13
  20. package/src/config.js +17 -0
  21. package/src/lib/domains.ts +94 -0
  22. package/src/lib/services/service-catalog.js +6 -10
  23. package/src/lib/services/services.js +37 -31
  24. package/src/lib/services-v2/service-catalog.ts +6 -11
  25. package/src/lib/services-v2/services-v2.ts +41 -31
  26. package/test/fixtures/activation-email.ts +22 -0
  27. package/test/integration/spec/services/service-catalog.js +7 -6
  28. package/test/integration/spec/services/services.js +11 -8
  29. package/test/integration/spec/services-v2/services-v2.js +11 -8
  30. package/test/unit/spec/interceptors/auth.js +56 -0
  31. package/test/unit/spec/services/service-catalog.js +93 -11
  32. package/test/unit/spec/services/services.js +6 -1
  33. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  34. package/test/unit/spec/services-v2/services-v2.ts +22 -1
  35. package/test/unit/spec/webex-core.js +0 -2
  36. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -0,0 +1,94 @@
1
+ import Url from 'url';
2
+
3
+ import {uniq} from 'lodash';
4
+
5
+ // Canonicalise a hostname for comparison: lowercase, drop the brackets around
6
+ // an IPv6 literal, and drop leading/trailing dots. DNS treats `Example.com`,
7
+ // `example.com.` and `example.com` as the same name.
8
+ //
9
+ // Node's `url.domainToASCII` looks like the standard way to do this, but the
10
+ // `url` polyfill this package bundles for the browser does not implement it,
11
+ // so it cannot be used here. It also leaves trailing dots in place.
12
+ const normalizeHostname = (value: string): string =>
13
+ typeof value === 'string'
14
+ ? value
15
+ .toLowerCase()
16
+ .replace(/^\[|\]$/g, '')
17
+ .replace(/^\.+/, '')
18
+ .replace(/\.+$/, '')
19
+ : '';
20
+
21
+ /**
22
+ * Canonicalise a list of configured allowed domains, discarding any entry that
23
+ * is not a usable hostname. Callers normalise on the way in so the stored list
24
+ * is already canonical, rather than re-deriving it on every request.
25
+ *
26
+ * @param {Array<string>} allowedDomains - The configured allowed domains.
27
+ * @returns {Array<string>} - Normalized, de-duplicated, non-empty entries.
28
+ */
29
+ export const normalizeAllowedDomains = (allowedDomains: Array<string>): Array<string> =>
30
+ uniq(
31
+ (Array.isArray(allowedDomains) ? allowedDomains : []).map(normalizeHostname).filter(Boolean)
32
+ );
33
+
34
+ /**
35
+ * Determine if a hostname is covered by an allowed domain, matching only on DNS
36
+ * label boundaries, so that a hostname is allowed only when it is the domain
37
+ * itself or a subdomain of it. Matching on a substring instead would treat
38
+ * unrelated hostnames that merely contain the domain as allowed.
39
+ *
40
+ * @param {string} hostname - Hostname to test. Must not include a port.
41
+ * @param {string} allowedDomain - The configured allowed domain.
42
+ * @returns {boolean} - True when the hostname is the domain or a subdomain of it.
43
+ */
44
+ const hostnameMatchesDomain = (hostname: string, allowedDomain: string): boolean => {
45
+ // The stored list is normalized on write, but `allowedDomains` is a public
46
+ // property, so normalize again here rather than trust it.
47
+ const host = normalizeHostname(hostname);
48
+ const domain = normalizeHostname(allowedDomain);
49
+
50
+ return !!host && !!domain && (host === domain || host.endsWith(`.${domain}`));
51
+ };
52
+
53
+ /**
54
+ * Find the allowed domain covering a url, or `undefined` if there is none.
55
+ *
56
+ * Parsing lives here rather than in the callers, and deliberately uses both url
57
+ * parsers, because this check gates an `Authorization` header. The two
58
+ * transports behind `@webex/http-core` do not use the same url parser: the
59
+ * browser transport parses per WHATWG, the node transport uses Node's legacy
60
+ * `Url.parse`, and for some inputs the two resolve different hosts.
61
+ *
62
+ * Rather than picking one, require both to agree and fail closed when they do
63
+ * not, so this check can never authorize a host that differs from the one a
64
+ * transport would actually connect to. Do not narrow this to a single parser.
65
+ *
66
+ * @param {string} url - The url to match the allowed domains against.
67
+ * @param {Array<string>} allowedDomains - The configured allowed domains.
68
+ * @returns {string} - The matching allowed domain, or undefined if there is none.
69
+ */
70
+ export const matchAllowedDomain = (
71
+ url: string,
72
+ allowedDomains: Array<string>
73
+ ): string | undefined => {
74
+ let hostname: string;
75
+ let legacyHostname: string;
76
+
77
+ try {
78
+ ({hostname} = new URL(url));
79
+ ({hostname: legacyHostname} = Url.parse(url));
80
+ } catch {
81
+ // Not a parsable absolute url, so it cannot belong to an allowed domain.
82
+ return undefined;
83
+ }
84
+
85
+ if (normalizeHostname(hostname) !== normalizeHostname(legacyHostname)) {
86
+ return undefined;
87
+ }
88
+
89
+ return (allowedDomains || []).find((allowedDomain) =>
90
+ hostnameMatchesDomain(hostname, allowedDomain)
91
+ );
92
+ };
93
+
94
+ export default matchAllowedDomain;
@@ -4,6 +4,7 @@ import AmpState from 'ampersand-state';
4
4
 
5
5
  import {union} from 'lodash';
6
6
  import ServiceUrl from './service-url';
7
+ import {matchAllowedDomain, normalizeAllowedDomains} from '../domains';
7
8
 
8
9
  /* eslint-disable no-underscore-dangle */
9
10
  /**
@@ -268,19 +269,14 @@ const ServiceCatalog = AmpState.extend({
268
269
  },
269
270
 
270
271
  /**
271
- * Finds an allowed domain that matches a specific url.
272
+ * Finds an allowed domain that matches a specific url. The url's hostname
273
+ * must be the allowed domain itself or a subdomain of it.
272
274
  *
273
275
  * @param {string} url - The url to match the allowed domains against.
274
276
  * @returns {string} - The matching allowed domain.
275
277
  */
276
278
  findAllowedDomain(url) {
277
- const urlObj = Url.parse(url);
278
-
279
- if (!urlObj.host) {
280
- return undefined;
281
- }
282
-
283
- return this.allowedDomains.find((allowedDomain) => urlObj.host.includes(allowedDomain));
279
+ return matchAllowedDomain(url, this.allowedDomains);
284
280
  },
285
281
 
286
282
  /**
@@ -367,7 +363,7 @@ const ServiceCatalog = AmpState.extend({
367
363
  * @returns {void}
368
364
  */
369
365
  setAllowedDomains(allowedDomains) {
370
- this.allowedDomains = [...allowedDomains];
366
+ this.allowedDomains = normalizeAllowedDomains(allowedDomains);
371
367
  },
372
368
 
373
369
  /**
@@ -376,7 +372,7 @@ const ServiceCatalog = AmpState.extend({
376
372
  * @returns {void}
377
373
  */
378
374
  addAllowedDomains(newAllowedDomains) {
379
- this.allowedDomains = union(this.allowedDomains, newAllowedDomains);
375
+ this.allowedDomains = union(this.allowedDomains, normalizeAllowedDomains(newAllowedDomains));
380
376
  },
381
377
 
382
378
  /**
@@ -23,12 +23,6 @@ const DEFAULT_CLUSTER_IDENTIFIER =
23
23
  const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
24
24
  const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
25
25
 
26
- // Maximum time we will wait for the initial catalog collection before letting
27
- // `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
28
- // must never leave the app on a permanent spinner - past this point downstream
29
- // consumers must fall through to their normal error/login paths.
30
- const SERVICES_INIT_TIMEOUT_MS = 15_000;
31
-
32
26
  /* eslint-disable no-underscore-dangle */
33
27
  /**
34
28
  * @class
@@ -1368,6 +1362,7 @@ const Services = WebexPlugin.extend({
1368
1362
 
1369
1363
  // Destructure the credentials plugin.
1370
1364
  const {credentials} = this.webex;
1365
+ const catalog = this._getCatalog();
1371
1366
 
1372
1367
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1373
1368
  // credentials#getOrgId() to properly throw.
@@ -1380,12 +1375,18 @@ const Services = WebexPlugin.extend({
1380
1375
  .then(() => {
1381
1376
  // Validate if the token is authorized.
1382
1377
  if (credentials.canAuthorize) {
1383
- // Attempt to collect the postauth catalog.
1384
-
1385
- return this.updateServices().catch(() => {
1386
- this.initFailed = true;
1387
- this.logger.warn('services: cannot retrieve postauth catalog');
1388
- });
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
+ });
1389
1390
  }
1390
1391
 
1391
1392
  // Return a resolved promise for consistent return value.
@@ -1480,16 +1481,14 @@ const Services = WebexPlugin.extend({
1480
1481
  const {supertoken} = this.webex.credentials;
1481
1482
  // Validate if the supertoken exists.
1482
1483
  if (supertoken && supertoken.access_token) {
1483
- this.initServiceCatalogs()
1484
- .then(() => {
1485
- catalog.isReady = true;
1486
- })
1487
- .catch((error) => {
1488
- this.initFailed = true;
1489
- this.logger.error(
1490
- `services: failed to init initial services when credentials available, ${error?.message}`
1491
- );
1492
- });
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
+ });
1493
1492
  } else {
1494
1493
  const {email} = this.webex.config;
1495
1494
 
@@ -1529,20 +1528,23 @@ const Services = WebexPlugin.extend({
1529
1528
 
1530
1529
  // Race init against a hard timeout so a hung request never leaves
1531
1530
  // `services.ready` false forever - that would stall `webex.ready` and
1532
- // leave the app on a permanent spinner.
1533
- const timeout = new Promise((_, reject) => {
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) => {
1534
1536
  setTimeout(
1535
- () => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
1536
- SERVICES_INIT_TIMEOUT_MS
1537
+ () => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
1538
+ initTimeoutMs
1537
1539
  );
1538
1540
  });
1539
1541
 
1540
1542
  // Validate if the supertoken exists.
1541
1543
  if (supertoken && supertoken.access_token) {
1542
- Promise.race([this.initServiceCatalogs(), timeout])
1543
- .then(() => {
1544
- catalog.isReady = true;
1545
- })
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])
1546
1548
  .catch((error) => {
1547
1549
  this.initFailed = true;
1548
1550
  this.logger.error(
@@ -1553,7 +1555,10 @@ const Services = WebexPlugin.extend({
1553
1555
  } else {
1554
1556
  const {email} = this.webex.config;
1555
1557
 
1556
- Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
1558
+ Promise.race([
1559
+ this.collectPreauthCatalog(email ? {email} : undefined),
1560
+ initServiceCatalogsTimeout,
1561
+ ])
1557
1562
  .catch((error) => {
1558
1563
  this.initFailed = true;
1559
1564
  this.logger.error(
@@ -1566,6 +1571,7 @@ const Services = WebexPlugin.extend({
1566
1571
  // for `canAuthorize` flipping true and then collect the postauth catalog.
1567
1572
  this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1568
1573
  if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1574
+ // `initServiceCatalogs` marks the catalog ready internally.
1569
1575
  this.initServiceCatalogs().catch((error) => {
1570
1576
  this.logger.error(
1571
1577
  `services: failed to init service catalogs after auth, ${error?.message}`
@@ -3,6 +3,7 @@ import AmpState from 'ampersand-state';
3
3
  import {union} from 'lodash';
4
4
  import ServiceDetail from './service-detail';
5
5
  import {IServiceDetail, ServiceGroup} from './types';
6
+ import {matchAllowedDomain, normalizeAllowedDomains} from '../domains';
6
7
 
7
8
  /**
8
9
  * @class
@@ -210,20 +211,14 @@ const ServiceCatalog = AmpState.extend({
210
211
  },
211
212
 
212
213
  /**
213
- * Finds an allowed domain that matches a specific url.
214
+ * Finds an allowed domain that matches a specific url. The url's hostname
215
+ * must be the allowed domain itself or a subdomain of it.
214
216
  *
215
217
  * @param {string} url - The url to match the allowed domains against.
216
218
  * @returns {string} - The matching allowed domain.
217
219
  */
218
220
  findAllowedDomain(url: string): string {
219
- try {
220
- const urlObj = new URL(url);
221
-
222
- return this.allowedDomains.find((allowedDomain) => urlObj.host.includes(allowedDomain));
223
- } catch {
224
- // If the URL is invalid or can't be found, return undefined
225
- return undefined;
226
- }
221
+ return matchAllowedDomain(url, this.allowedDomains);
227
222
  },
228
223
 
229
224
  /**
@@ -282,7 +277,7 @@ const ServiceCatalog = AmpState.extend({
282
277
  * @returns {void}
283
278
  */
284
279
  setAllowedDomains(allowedDomains: Array<string>): void {
285
- this.allowedDomains = [...allowedDomains];
280
+ this.allowedDomains = normalizeAllowedDomains(allowedDomains);
286
281
  },
287
282
 
288
283
  /**
@@ -291,7 +286,7 @@ const ServiceCatalog = AmpState.extend({
291
286
  * @returns {void}
292
287
  */
293
288
  addAllowedDomains(newAllowedDomains: Array<string>): void {
294
- this.allowedDomains = union(this.allowedDomains, newAllowedDomains);
289
+ this.allowedDomains = union(this.allowedDomains, normalizeAllowedDomains(newAllowedDomains));
295
290
  },
296
291
 
297
292
  /**
@@ -32,12 +32,6 @@ const DEFAULT_CLUSTER_IDENTIFIER =
32
32
  const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
33
33
  const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
34
34
 
35
- // Maximum time we will wait for the initial catalog collection before letting
36
- // `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
37
- // must never leave the app on a permanent spinner - past this point downstream
38
- // consumers must fall through to their normal error/login paths.
39
- const SERVICES_INIT_TIMEOUT_MS = 15_000;
40
-
41
35
  /* eslint-disable no-underscore-dangle */
42
36
  /**
43
37
  * @class
@@ -1241,7 +1235,11 @@ const Services = WebexPlugin.extend({
1241
1235
  ): Promise<object> {
1242
1236
  const service = 'u2c';
1243
1237
  const resource = from ? `/${from}/catalog` : '/catalog';
1244
- const qs = {...(query || {}), format: 'U2CV2'};
1238
+ const qs = {
1239
+ ...(query || {}),
1240
+ format: 'U2CV2',
1241
+ ...(this.webex.config?.services?.useCatalogOverride && {useCatalogOverride: true}),
1242
+ };
1245
1243
 
1246
1244
  if (forceRefresh) {
1247
1245
  qs.timestamp = new Date().getTime();
@@ -1334,6 +1332,7 @@ const Services = WebexPlugin.extend({
1334
1332
 
1335
1333
  // Destructure the credentials plugin.
1336
1334
  const {credentials} = this.webex;
1335
+ const catalog = this._getCatalog();
1337
1336
 
1338
1337
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1339
1338
  // credentials#getOrgId() to properly throw.
@@ -1346,11 +1345,18 @@ const Services = WebexPlugin.extend({
1346
1345
  .then(() => {
1347
1346
  // Validate if the token is authorized.
1348
1347
  if (credentials.canAuthorize) {
1349
- // Attempt to collect the postauth catalog.
1350
- return this.updateServices({forceRefresh: refresh}).catch(() => {
1351
- this.initFailed = true;
1352
- this.logger.warn('services: cannot retrieve postauth catalog');
1353
- });
1348
+ // Attempt to collect the postauth catalog, then mark the catalog
1349
+ // ready. Setting `isReady` here - rather than only in the init
1350
+ // callers - means a slow postauth fetch that loses the gated-init
1351
+ // timeout race still marks the catalog ready once it completes.
1352
+ return this.updateServices({forceRefresh: refresh})
1353
+ .then(() => {
1354
+ catalog.isReady = true;
1355
+ })
1356
+ .catch(() => {
1357
+ this.initFailed = true;
1358
+ this.logger.warn('services: cannot retrieve postauth catalog');
1359
+ });
1354
1360
  }
1355
1361
 
1356
1362
  // Return a resolved promise for consistent return value.
@@ -1438,16 +1444,14 @@ const Services = WebexPlugin.extend({
1438
1444
  const {supertoken} = this.webex.credentials;
1439
1445
  // Validate if the supertoken exists.
1440
1446
  if (supertoken && supertoken.access_token) {
1441
- this.initServiceCatalogs()
1442
- .then(() => {
1443
- catalog.isReady = true;
1444
- })
1445
- .catch((error) => {
1446
- this.initFailed = true;
1447
- this.logger.error(
1448
- `services: failed to init initial services when credentials available, ${error?.message}`
1449
- );
1450
- });
1447
+ // `initServiceCatalogs` marks the catalog ready internally once the
1448
+ // postauth catalog is collected.
1449
+ this.initServiceCatalogs().catch((error) => {
1450
+ this.initFailed = true;
1451
+ this.logger.error(
1452
+ `services: failed to init initial services when credentials available, ${error?.message}`
1453
+ );
1454
+ });
1451
1455
  } else {
1452
1456
  const {email} = this.webex.config;
1453
1457
 
@@ -1487,20 +1491,22 @@ const Services = WebexPlugin.extend({
1487
1491
 
1488
1492
  // Race init against a hard timeout so a hung request never leaves
1489
1493
  // `services.ready` false forever - that would stall `webex.ready` and
1490
- // leave the app on a permanent spinner.
1491
- const timeout = new Promise<never>((_, reject) => {
1494
+ // leave consumers waiting on it indefinitely. Timeout is configurable via
1495
+ // `config.services.catalogInitTimeout` (defaults to 15s in config).
1496
+ const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
1497
+ const initServiceCatalogsTimeout = new Promise<never>((_, reject) => {
1492
1498
  setTimeout(
1493
- () => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
1494
- SERVICES_INIT_TIMEOUT_MS
1499
+ () => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
1500
+ initTimeoutMs
1495
1501
  );
1496
1502
  });
1497
1503
 
1498
1504
  // Validate if the supertoken exists.
1499
1505
  if (supertoken && supertoken.access_token) {
1500
- Promise.race([this.initServiceCatalogs(), timeout])
1501
- .then(() => {
1502
- catalog.isReady = true;
1503
- })
1506
+ // `initServiceCatalogs` marks the catalog ready internally once the
1507
+ // postauth catalog is collected - even if it loses the timeout race
1508
+ // above, so a slow fetch still eventually flips `catalog.isReady`.
1509
+ Promise.race([this.initServiceCatalogs(), initServiceCatalogsTimeout])
1504
1510
  .catch((error) => {
1505
1511
  this.initFailed = true;
1506
1512
  this.logger.error(
@@ -1511,7 +1517,10 @@ const Services = WebexPlugin.extend({
1511
1517
  } else {
1512
1518
  const {email} = this.webex.config;
1513
1519
 
1514
- Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
1520
+ Promise.race([
1521
+ this.collectPreauthCatalog(email ? {email} : undefined),
1522
+ initServiceCatalogsTimeout,
1523
+ ])
1515
1524
  .catch((error) => {
1516
1525
  this.initFailed = true;
1517
1526
  this.logger.error(
@@ -1524,6 +1533,7 @@ const Services = WebexPlugin.extend({
1524
1533
  // for `canAuthorize` flipping true and then collect the postauth catalog.
1525
1534
  this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1526
1535
  if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1536
+ // `initServiceCatalogs` marks the catalog ready internally.
1527
1537
  this.initServiceCatalogs().catch((error) => {
1528
1538
  this.logger.error(
1529
1539
  `services: failed to init service catalogs after auth, ${error?.message}`
@@ -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;
@@ -23,12 +23,13 @@ describe('webex-core', () => {
23
23
  .then(
24
24
  ([user]) =>
25
25
  new Promise((resolve) => {
26
- webexUser = user;
27
- webex = new WebexCore({credentials: user.token});
28
- services = webex.internal.services;
29
- catalog = services._getCatalog();
30
- // Wait for webex ready event before registering device to ensure newMetrics.callDiagnosticMetrics is initialized
31
- webex.once('ready', resolve);
26
+ setTimeout(() => {
27
+ webexUser = user;
28
+ webex = new WebexCore({credentials: user.token});
29
+ services = webex.internal.services;
30
+ catalog = services._getCatalog();
31
+ resolve();
32
+ }, 1000);
32
33
  })
33
34
  )
34
35
  .then(() => webex.internal.device.register())
@@ -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', () =>
@@ -925,7 +928,7 @@ describe('webex-core', () => {
925
928
 
926
929
  it('validates a non-existing user', () =>
927
930
  unauthServices
928
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
931
+ .validateUser({email: createActivationEmail()})
929
932
  .then((r) => {
930
933
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
931
934
  assert.equal(r.activated, false);
@@ -938,7 +941,7 @@ describe('webex-core', () => {
938
941
  it('validates new user with activationOptions suppressEmail false', () =>
939
942
  unauthServices
940
943
  .validateUser({
941
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
944
+ email: createActivationEmail(),
942
945
  activationOptions: {suppressEmail: false},
943
946
  })
944
947
  .then((r) => {
@@ -954,7 +957,7 @@ describe('webex-core', () => {
954
957
  it('validates new user with activationOptions suppressEmail true', () =>
955
958
  unauthServices
956
959
  .validateUser({
957
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
960
+ email: createActivationEmail(),
958
961
  activationOptions: {suppressEmail: true},
959
962
  })
960
963
  .then((r) => {
@@ -1010,7 +1013,7 @@ describe('webex-core', () => {
1010
1013
 
1011
1014
  return unauthServices
1012
1015
  .validateUser({
1013
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1016
+ email: createActivationEmail(),
1014
1017
  activationOptions: {suppressEmail: true},
1015
1018
  })
1016
1019
  .then(() => {
@@ -1024,7 +1027,7 @@ describe('webex-core', () => {
1024
1027
 
1025
1028
  return unauthServices
1026
1029
  .validateUser({
1027
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1030
+ email: createActivationEmail(),
1028
1031
  activationOptions: {suppressEmail: true},
1029
1032
  preloginUserId,
1030
1033
  })
@@ -1041,7 +1044,7 @@ describe('webex-core', () => {
1041
1044
 
1042
1045
  return unauthServices
1043
1046
  .validateUser({
1044
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1047
+ email: createActivationEmail(),
1045
1048
  activationOptions: {suppressEmail: true},
1046
1049
  })
1047
1050
  .then(() => {
@@ -1064,7 +1067,7 @@ describe('webex-core', () => {
1064
1067
 
1065
1068
  return userOnboardingServices
1066
1069
  .validateUser({
1067
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1070
+ email: createActivationEmail(),
1068
1071
  activationOptions: {suppressEmail: true},
1069
1072
  })
1070
1073
  .then(() => {
@@ -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', () =>
@@ -848,7 +851,7 @@ describe('webex-core', () => {
848
851
 
849
852
  it('validates a non-existing user', () =>
850
853
  unauthServices
851
- .validateUser({email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
854
+ .validateUser({email: createActivationEmail()})
852
855
  .then((r) => {
853
856
  assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
854
857
  assert.equal(r.activated, false);
@@ -858,7 +861,7 @@ describe('webex-core', () => {
858
861
  it('validates new user with activationOptions suppressEmail false', () =>
859
862
  unauthServices
860
863
  .validateUser({
861
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
864
+ email: createActivationEmail(),
862
865
  activationOptions: {suppressEmail: false},
863
866
  })
864
867
  .then((r) => {
@@ -871,7 +874,7 @@ describe('webex-core', () => {
871
874
  it('validates new user with activationOptions suppressEmail true', () =>
872
875
  unauthServices
873
876
  .validateUser({
874
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
877
+ email: createActivationEmail(),
875
878
  activationOptions: {suppressEmail: true},
876
879
  })
877
880
  .then((r) => {
@@ -915,7 +918,7 @@ describe('webex-core', () => {
915
918
 
916
919
  return unauthServices
917
920
  .validateUser({
918
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
921
+ email: createActivationEmail(),
919
922
  activationOptions: {suppressEmail: true},
920
923
  })
921
924
  .then(() => {
@@ -929,7 +932,7 @@ describe('webex-core', () => {
929
932
 
930
933
  return unauthServices
931
934
  .validateUser({
932
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
935
+ email: createActivationEmail(),
933
936
  activationOptions: {suppressEmail: true},
934
937
  preloginUserId,
935
938
  })
@@ -946,7 +949,7 @@ describe('webex-core', () => {
946
949
 
947
950
  return unauthServices
948
951
  .validateUser({
949
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
952
+ email: createActivationEmail(),
950
953
  activationOptions: {suppressEmail: true},
951
954
  })
952
955
  .then(() => {
@@ -969,7 +972,7 @@ describe('webex-core', () => {
969
972
 
970
973
  return userOnboardingServices
971
974
  .validateUser({
972
- email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
975
+ email: createActivationEmail(),
973
976
  activationOptions: {suppressEmail: true},
974
977
  })
975
978
  .then(() => {