@webex/webex-core 3.12.0-next.4 → 3.12.0-next.40

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 (43) 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/credentials-config.js +12 -0
  5. package/dist/credentials-config.js.map +1 -1
  6. package/dist/lib/batcher.js +23 -7
  7. package/dist/lib/batcher.js.map +1 -1
  8. package/dist/lib/credentials/credentials.js +48 -4
  9. package/dist/lib/credentials/credentials.js.map +1 -1
  10. package/dist/lib/credentials/token.js +1 -1
  11. package/dist/lib/domains.js +90 -0
  12. package/dist/lib/domains.js.map +1 -0
  13. package/dist/lib/services/service-catalog.js +6 -10
  14. package/dist/lib/services/service-catalog.js.map +1 -1
  15. package/dist/lib/services/services.js +196 -28
  16. package/dist/lib/services/services.js.map +1 -1
  17. package/dist/lib/services-v2/service-catalog.js +6 -12
  18. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  19. package/dist/lib/services-v2/services-v2.js +203 -30
  20. package/dist/lib/services-v2/services-v2.js.map +1 -1
  21. package/dist/plugins/logger.js +1 -1
  22. package/dist/webex-core.js +2 -2
  23. package/dist/webex-core.js.map +1 -1
  24. package/package.json +13 -13
  25. package/src/config.js +17 -0
  26. package/src/credentials-config.js +13 -0
  27. package/src/lib/batcher.js +25 -10
  28. package/src/lib/credentials/credentials.js +50 -3
  29. package/src/lib/domains.ts +94 -0
  30. package/src/lib/services/service-catalog.js +6 -10
  31. package/src/lib/services/services.js +170 -17
  32. package/src/lib/services-v2/service-catalog.ts +6 -11
  33. package/src/lib/services-v2/services-v2.ts +174 -17
  34. package/test/fixtures/activation-email.ts +22 -0
  35. package/test/integration/spec/services/services.js +44 -8
  36. package/test/integration/spec/services-v2/services-v2.js +44 -8
  37. package/test/unit/spec/credentials/credentials.js +133 -2
  38. package/test/unit/spec/interceptors/auth.js +56 -0
  39. package/test/unit/spec/lib/batcher.js +56 -0
  40. package/test/unit/spec/services/service-catalog.js +93 -11
  41. package/test/unit/spec/services/services.js +307 -2
  42. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  43. package/test/unit/spec/services-v2/services-v2.ts +311 -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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`})
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
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: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
975
+ email: createActivationEmail(),
940
976
  activationOptions: {suppressEmail: true},
941
977
  })
942
978
  .then(() => {
@@ -183,6 +183,10 @@ describe('webex-core', () => {
183
183
  webex.credentials.buildLoginUrl({state: 'state'});
184
184
  }, /if specified, `options.state` must be an object/);
185
185
 
186
+ assert.throws(() => {
187
+ webex.credentials.buildLoginUrl({state: null});
188
+ }, /if specified, `options.state` must be an object/);
189
+
186
190
  assert.doesNotThrow(() => {
187
191
  webex.credentials.buildLoginUrl({state: {}});
188
192
  }, /if specified, `options.state` must be an object/);
@@ -232,6 +236,132 @@ describe('webex-core', () => {
232
236
  });
233
237
  });
234
238
 
239
+ describe('#buildThirdPartyLoginUrl()', () => {
240
+ it('throws if both `oauth2provider` and `returnURL` are missing', () => {
241
+ const webex = new MockWebex();
242
+ const credentials = new Credentials(undefined, {parent: webex});
243
+
244
+ webex.trigger('change:config');
245
+
246
+ assert.throws(() => {
247
+ credentials.buildThirdPartyLoginUrl({});
248
+ }, /`options.oauth2provider` is required/);
249
+ });
250
+
251
+ it('throws if `oauth2provider` is missing', () => {
252
+ const webex = new MockWebex();
253
+ const credentials = new Credentials(undefined, {parent: webex});
254
+
255
+ webex.trigger('change:config');
256
+
257
+ assert.throws(() => {
258
+ credentials.buildThirdPartyLoginUrl({returnURL: 'https://web.webex.com'});
259
+ }, /`options.oauth2provider` is required/);
260
+ });
261
+
262
+ it('throws if `returnURL` is missing', () => {
263
+ const webex = new MockWebex();
264
+ const credentials = new Credentials(undefined, {parent: webex});
265
+
266
+ webex.trigger('change:config');
267
+
268
+ assert.throws(() => {
269
+ credentials.buildThirdPartyLoginUrl({oauth2provider: 'google'});
270
+ }, /`options.returnURL` is required/);
271
+ });
272
+
273
+ skipInBrowser(it)('generates the third-party login url', () => {
274
+ const webex = new MockWebex();
275
+ const credentials = new Credentials(undefined, {parent: webex});
276
+
277
+ webex.trigger('change:config');
278
+
279
+ assert.equal(
280
+ credentials.buildThirdPartyLoginUrl({
281
+ oauth2provider: 'google',
282
+ returnURL: 'https://web.webex.com',
283
+ }),
284
+ `${
285
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
286
+ }/idb/ThirdPartyLogin?oauth2provider=google&returnURL=https%3A%2F%2Fweb.webex.com`
287
+ );
288
+ });
289
+
290
+ skipInBrowser(it)('generates the url with different parameter values', () => {
291
+ const webex = new MockWebex();
292
+ const credentials = new Credentials(undefined, {parent: webex});
293
+
294
+ webex.trigger('change:config');
295
+
296
+ assert.equal(
297
+ credentials.buildThirdPartyLoginUrl({
298
+ oauth2provider: 'apple',
299
+ returnURL: 'https://example.com/callback',
300
+ }),
301
+ `${
302
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
303
+ }/idb/ThirdPartyLogin?oauth2provider=apple&returnURL=https%3A%2F%2Fexample.com%2Fcallback`
304
+ );
305
+ });
306
+
307
+ it('throws if `state` is not an object', () => {
308
+ const webex = new MockWebex();
309
+ const credentials = new Credentials(undefined, {parent: webex});
310
+
311
+ webex.trigger('change:config');
312
+
313
+ assert.throws(() => {
314
+ credentials.buildThirdPartyLoginUrl({
315
+ oauth2provider: 'google',
316
+ returnURL: 'https://web.webex.com',
317
+ state: 'not-an-object',
318
+ });
319
+ }, /`options.state` must be an object/);
320
+ });
321
+
322
+ skipInBrowser(it)('omits `state` when an empty object is provided', () => {
323
+ const webex = new MockWebex();
324
+ const credentials = new Credentials(undefined, {parent: webex});
325
+
326
+ webex.trigger('change:config');
327
+
328
+ const result = credentials.buildThirdPartyLoginUrl({
329
+ oauth2provider: 'google',
330
+ returnURL: 'https://web.webex.com',
331
+ state: {},
332
+ });
333
+
334
+ const parsed = new URL(result);
335
+
336
+ assert.isFalse(parsed.searchParams.has('state'));
337
+ });
338
+
339
+ skipInBrowser(it)('base64url-encodes a non-empty `state` and emits it as a top-level query param', () => {
340
+ const webex = new MockWebex();
341
+ const credentials = new Credentials(undefined, {parent: webex});
342
+
343
+ webex.trigger('change:config');
344
+
345
+ const result = credentials.buildThirdPartyLoginUrl({
346
+ oauth2provider: 'google',
347
+ returnURL: 'https://web.webex.com',
348
+ state: {csrf_token: 'abc', popUpSignIn: true},
349
+ });
350
+
351
+ // Literal base64url of '{"csrf_token":"abc","popUpSignIn":true}'
352
+ const expectedState = 'eyJjc3JmX3Rva2VuIjoiYWJjIiwicG9wVXBTaWduSW4iOnRydWV9';
353
+
354
+ assert.equal(
355
+ result,
356
+ `${
357
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
358
+ }/idb/ThirdPartyLogin?oauth2provider=google&returnURL=${encodeURIComponent(
359
+ 'https://web.webex.com'
360
+ )}&state=${expectedState}`
361
+ );
362
+ });
363
+ });
364
+
235
365
  describe('#buildLogoutUrl()', () => {
236
366
  skipInBrowser(it)('generates the logout url', () => {
237
367
  const webex = new MockWebex();
@@ -368,7 +498,9 @@ describe('webex-core', () => {
368
498
  });
369
499
 
370
500
  it('should throw when provided an invalid token', () =>
371
- expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format, token has 1 sections'));
501
+ expect(() => credentials.extractOrgIdFromUserToken()).toThrow(
502
+ 'the provided token is not a valid format, token has 1 sections'
503
+ ));
372
504
 
373
505
  it('should throw when no token is provided', () =>
374
506
  expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
@@ -799,7 +931,6 @@ describe('webex-core', () => {
799
931
  .then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
800
932
  });
801
933
 
802
-
803
934
  // it('does not induce any token refreshes');
804
935
 
805
936
  it('prevents #getUserToken() from being invoked', () => {
@@ -14,6 +14,8 @@ import {
14
14
  AuthInterceptor,
15
15
  config,
16
16
  Credentials,
17
+ Services,
18
+ ServicesV2,
17
19
  WebexHttpError,
18
20
  Token,
19
21
  serviceConstants,
@@ -380,6 +382,60 @@ describe('webex-core', () => {
380
382
  });
381
383
  });
382
384
 
385
+ describe('#onRequest() against a real service catalog', () => {
386
+ // The cases above stub `isAllowedDomainUrl`, so they answer the
387
+ // allowed-domain question themselves and cannot show that the catalog
388
+ // matcher is reached by the code that attaches the token. These use a
389
+ // real catalog and real allowed-domain methods.
390
+ [
391
+ {name: 'Services', Constructor: Services},
392
+ {name: 'ServicesV2', Constructor: ServicesV2},
393
+ ].forEach(({name, Constructor}) => {
394
+ describe(name, () => {
395
+ let getUserToken;
396
+
397
+ beforeEach(() => {
398
+ const services = new Constructor(undefined, {parent: webex});
399
+
400
+ services._getCatalog().setAllowedDomains(['webex.com']);
401
+ // the catalog holds no services, so a url that is not covered by
402
+ // an allowed domain has nothing else to authorize it
403
+ services.waitForService = sinon.stub().rejects(new Error('no such service'));
404
+
405
+ webex.internal.services = services;
406
+ getUserToken = sinon.spy(webex.credentials, 'getUserToken');
407
+ });
408
+
409
+ afterEach(() => {
410
+ getUserToken.restore();
411
+ delete webex.internal.services;
412
+ });
413
+
414
+ it('adds the authorization header for a url under an allowed domain', () =>
415
+ interceptor
416
+ .onRequest({uri: 'https://api.webex.com/resource', headers: {}})
417
+ .then((options) => {
418
+ assert.equal(
419
+ options.headers.authorization,
420
+ webex.credentials.supertoken.toString()
421
+ );
422
+ assert.calledOnce(getUserToken);
423
+ }));
424
+
425
+ [
426
+ 'https://notwebex.com/resource',
427
+ 'https://webex.com.unrelated.example/resource',
428
+ ].forEach((uri) => {
429
+ it(`does not add the authorization header for ${uri}`, () =>
430
+ interceptor.onRequest({uri, headers: {}}).then((options) => {
431
+ assert.notProperty(options.headers, 'authorization');
432
+ assert.notCalled(getUserToken);
433
+ }));
434
+ });
435
+ });
436
+ });
437
+ });
438
+
383
439
  describe('#onResponseError()', () => {
384
440
  describe('when the server responds with 401', () => {
385
441
  nodeOnly(it)('refreshes the access token and replays the request', () => {
@@ -7,6 +7,7 @@ import {assert} from '@webex/test-helper-chai';
7
7
  import MockWebex from '@webex/test-helper-mock-webex';
8
8
  import sinon from 'sinon';
9
9
  import {Batcher} from '@webex/webex-core';
10
+ import WebexHttpError from '../../../../src/lib/webex-http-error';
10
11
 
11
12
  function promiseTick(count) {
12
13
  let promise = Promise.resolve();
@@ -154,6 +155,61 @@ describe('webex-core', () => {
154
155
  return Promise.all([assert.isRejected(p1), assert.isRejected(p2)]);
155
156
  });
156
157
  });
158
+
159
+ it('does not trigger unhandledRejection when caller handles rejection', () => {
160
+ const unhandled = [];
161
+ const onUnhandledRejection = (reason) => {
162
+ unhandled.push(reason);
163
+ };
164
+
165
+ process.on('unhandledRejection', onUnhandledRejection);
166
+
167
+ const p = webex.internal.batcher.request(1);
168
+
169
+ // eslint-disable-next-line prefer-promise-reject-errors
170
+ webex.request.returns(Promise.reject({statusCode: 0}));
171
+
172
+ return promiseTick(50)
173
+ .then(() => clock.tick(2))
174
+ .then(() => promiseTick(50))
175
+ .then(() => assert.isRejected(p))
176
+ .then(() => promiseTick(50))
177
+ .then(() => {
178
+ assert.lengthOf(unhandled, 0);
179
+ })
180
+ .finally(() => {
181
+ process.removeListener('unhandledRejection', onUnhandledRejection);
182
+ });
183
+ });
184
+
185
+ it('fails queued deferreds for webex http errors without request body', () => {
186
+ const p1 = webex.internal.batcher.request(1);
187
+ const p2 = webex.internal.batcher.request(2);
188
+ const reason = new WebexHttpError.BadRequest({
189
+ statusCode: 400,
190
+ body: {message: 'simulated failure'},
191
+ options: {
192
+ method: 'GET',
193
+ uri: 'https://example.com/v1/mock/batch',
194
+ headers: {trackingid: 'test-tracking-id'},
195
+ },
196
+ headers: {},
197
+ });
198
+
199
+ webex.request.returns(Promise.reject(reason));
200
+
201
+ return promiseTick(50)
202
+ .then(() => clock.tick(2))
203
+ .then(() => promiseTick(50))
204
+ .then(() => {
205
+ assert.calledOnce(webex.request);
206
+
207
+ return Promise.all([
208
+ assert.isRejected(p1, /simulated failure/),
209
+ assert.isRejected(p2, /simulated failure/),
210
+ ]);
211
+ });
212
+ });
157
213
  });
158
214
 
159
215
  describe('when the number of request attempts exceeds a given threshold', () => {
@@ -101,7 +101,24 @@ describe('webex-core', () => {
101
101
  const domains = [];
102
102
 
103
103
  beforeEach(() => {
104
- domains.push('example-a', 'example-b', 'example-c');
104
+ // Shaped like a real catalog rather than a tidy list: the commercial
105
+ // domains the sdk ships with, a multi-part suffix, sites added at
106
+ // runtime from meeting preferences, an entry that overlaps another,
107
+ // and an unset entry, which callers can add and which must match
108
+ // nothing.
109
+ domains.push(
110
+ 'wbx2.com',
111
+ 'ciscospark.com',
112
+ 'webex.com',
113
+ 'webexapis.com',
114
+ 'broadcloud.com.au',
115
+ 'webexgov.us',
116
+ 'example-a.com',
117
+ 'example-b.com',
118
+ 'example-c.com',
119
+ 'go.example-a.com',
120
+ ''
121
+ );
105
122
 
106
123
  catalog.setAllowedDomains(domains);
107
124
  });
@@ -110,10 +127,60 @@ describe('webex-core', () => {
110
127
  domains.length = 0;
111
128
  });
112
129
 
113
- it('finds an allowed domain that matches a specific url', () => {
114
- const domain = catalog.findAllowedDomain('http://example-a.com/resource/id');
115
-
116
- assert.include(domains, domain);
130
+ [
131
+ // real service hosts resolve to the entry that covers them
132
+ ['https://u2c-a.wbx2.com/u2c/api/v1/limited/catalog', 'wbx2.com'],
133
+ ['https://conv-a.wbx2.com/conversation/api/v1/conversations', 'wbx2.com'],
134
+ ['https://foobar.ciscospark.com/resource/id', 'ciscospark.com'],
135
+ ['https://idbroker.webex.com/idb/token', 'webex.com'],
136
+ ['https://webexapis.com/v1/people/me', 'webexapis.com'],
137
+ // an entry is not required to be a bare registrable domain
138
+ ['https://foo.broadcloud.com.au/resource/id', 'broadcloud.com.au'],
139
+ ['https://a.b.webexgov.us/resource/id', 'webexgov.us'],
140
+ // the domain itself and its subdomains match
141
+ ['https://example-a.com/resource/id', 'example-a.com'],
142
+ ['https://sub.example-b.com/resource/id', 'example-b.com'],
143
+ ['https://deep.sub.example-c.com/resource/id', 'example-c.com'],
144
+ // overlapping entries resolve to the first covering entry in the list
145
+ ['https://go.example-a.com/resource/id', 'example-a.com'],
146
+ // an explicit port must not defeat the match
147
+ ['https://example-a.com:8443/resource/id', 'example-a.com'],
148
+ ['https://u2c-a.wbx2.com:8000/resource/id', 'wbx2.com'],
149
+ // hostname comparison is case insensitive
150
+ ['https://U2C-A.WBX2.COM/resource/id', 'wbx2.com'],
151
+ // a trailing dot is the same name
152
+ ['https://u2c-a.wbx2.com./resource/id', 'wbx2.com'],
153
+ // a sibling registrable domain must not match
154
+ ['https://notwebex.com/resource/id', undefined],
155
+ ['https://mywebexgov.us/resource/id', undefined],
156
+ ['https://webex.company/resource/id', undefined],
157
+ ['https://webex.com-unrelated.example/resource/id', undefined],
158
+ ['https://example-a.community/resource/id', undefined],
159
+ // a partial label must not match, even against a multi-part entry
160
+ ['https://broadcloud.com/resource/id', undefined],
161
+ // the domain matches only as a suffix, on a label boundary
162
+ ['https://webex.com.unrelated.example/resource/id', undefined],
163
+ ['https://unrelated.example/webex.com/resource/id', undefined],
164
+ ['https://unrelated.example/?next=https://webex.com', undefined],
165
+ // userinfo is not the host
166
+ ['https://webex.com@unrelated.example/resource/id', undefined],
167
+ // an encoded or unusual separator is not a label boundary, and the url
168
+ // parsers used by the transports do not agree on where these end the
169
+ // host, so they must not resolve to an allowed domain
170
+ ['https://webex.com%2eunrelated.example/resource/id', undefined],
171
+ ['https://webex.com%2Eunrelated.example/resource/id', undefined],
172
+ ['https://unrelated.example%2ewebex.com/resource/id', undefined],
173
+ ['https://unrelated.example%2Ewebex.com/resource/id', undefined],
174
+ ['https://unrelated.example;.webex.com/resource/id', undefined],
175
+ // an empty allowed domain entry matches nothing
176
+ ['https://unrelated.example/resource/id', undefined],
177
+ // unparseable urls
178
+ ['', undefined],
179
+ ['not a url', undefined],
180
+ ].forEach(([url, expected]) => {
181
+ it(`returns ${expected} for ${url || '<empty>'}`, () => {
182
+ assert.equal(catalog.findAllowedDomain(url), expected);
183
+ });
117
184
  });
118
185
  });
119
186
 
@@ -121,7 +188,7 @@ describe('webex-core', () => {
121
188
  const domains = [];
122
189
 
123
190
  beforeEach(() => {
124
- domains.push('example-a', 'example-b', 'example-c');
191
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
125
192
 
126
193
  catalog.setAllowedDomains(domains);
127
194
  });
@@ -160,7 +227,7 @@ describe('webex-core', () => {
160
227
  const domains = [];
161
228
 
162
229
  beforeEach(() => {
163
- domains.push('example-a', 'example-b', 'example-c');
230
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
164
231
 
165
232
  catalog.setAllowedDomains(domains);
166
233
  });
@@ -170,19 +237,31 @@ describe('webex-core', () => {
170
237
  });
171
238
 
172
239
  it('sets the allowed domain entries to new values', () => {
173
- const newValues = ['example-d', 'example-e', 'example-f'];
240
+ const newValues = ['example-d.com', 'example-e.com', 'example-f.com'];
174
241
 
175
242
  catalog.setAllowedDomains(newValues);
176
243
 
177
244
  assert.notDeepInclude(domains, newValues);
178
245
  });
246
+
247
+ it('canonicalizes entries and discards those that are not usable', () => {
248
+ catalog.setAllowedDomains([
249
+ 'Example-D.COM',
250
+ 'example-d.com',
251
+ 'example-e.com.',
252
+ '',
253
+ undefined,
254
+ ]);
255
+
256
+ assert.deepEqual(catalog.getAllowedDomains(), ['example-d.com', 'example-e.com']);
257
+ });
179
258
  });
180
259
 
181
260
  describe('#addAllowedDomains()', () => {
182
261
  const domains = [];
183
262
 
184
263
  beforeEach(() => {
185
- domains.push('example-a', 'example-b', 'example-c');
264
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
186
265
 
187
266
  catalog.setAllowedDomains(domains);
188
267
  });
@@ -192,13 +271,16 @@ describe('webex-core', () => {
192
271
  });
193
272
 
194
273
  it('merge the allowed domain entries with new values', () => {
195
- const newValues = ['example-c', 'example-e', 'example-f'];
274
+ const newValues = ['example-c.com', 'example-e.com', 'example-f.com'];
196
275
 
197
276
  catalog.addAllowedDomains(newValues);
198
277
 
199
278
  const list = catalog.getAllowedDomains();
200
279
 
201
- assert.match(['example-a', 'example-b', 'example-c', 'example-e', 'example-f'], list);
280
+ assert.match(
281
+ ['example-a.com', 'example-b.com', 'example-c.com', 'example-e.com', 'example-f.com'],
282
+ list
283
+ );
202
284
  });
203
285
  });
204
286