@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/services/service-url.js +11 -1
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +501 -96
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +426 -42
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/lib/services-v2/types.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +7 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/services/service-url.js +9 -1
- package/src/lib/services/services.js +368 -14
- package/src/lib/services-v2/services-v2.ts +353 -10
- package/src/lib/services-v2/types.ts +5 -0
- package/test/integration/spec/services/service-catalog.js +16 -11
- package/test/integration/spec/services/services.js +38 -11
- package/test/integration/spec/services-v2/services-v2.js +29 -8
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-url.js +110 -0
- package/test/unit/spec/services/services.js +680 -80
- package/test/unit/spec/services-v2/services-v2.ts +484 -62
- package/test/unit/spec/webex-core.js +2 -0
- package/test/unit/spec/webex-internal-core.js +2 -0
|
@@ -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(
|
|
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', () => {
|
|
@@ -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', () => {
|
|
@@ -151,6 +151,116 @@ describe('webex-core', () => {
|
|
|
151
151
|
|
|
152
152
|
assert.isTrue(homeClusterUrls.every((host) => !host.failed));
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
describe('when hosts have negative priorities', () => {
|
|
156
|
+
it('should return defaultUrl when all hosts have negative priorities', () => {
|
|
157
|
+
const negativeServiceUrl = new ServiceUrl({
|
|
158
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
159
|
+
hosts: [
|
|
160
|
+
{
|
|
161
|
+
host: 'example-host-neg1.com',
|
|
162
|
+
priority: -1,
|
|
163
|
+
ttl: -1,
|
|
164
|
+
id: '1',
|
|
165
|
+
homeCluster: true,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
host: 'example-host-neg2.com',
|
|
169
|
+
priority: -1,
|
|
170
|
+
ttl: -1,
|
|
171
|
+
id: '2',
|
|
172
|
+
homeCluster: true,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
name: 'negative-priority-test',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.equal(
|
|
179
|
+
negativeServiceUrl._getPriorityHostUrl(),
|
|
180
|
+
'https://default.example.com/api/v1'
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return defaultUrl when all hosts have zero priority', () => {
|
|
185
|
+
const zeroServiceUrl = new ServiceUrl({
|
|
186
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
187
|
+
hosts: [
|
|
188
|
+
{
|
|
189
|
+
host: 'example-host-zero.com',
|
|
190
|
+
priority: 0,
|
|
191
|
+
ttl: -1,
|
|
192
|
+
id: '1',
|
|
193
|
+
homeCluster: true,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
name: 'zero-priority-test',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
assert.equal(zeroServiceUrl._getPriorityHostUrl(), 'https://default.example.com/api/v1');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should ignore hosts with negative priorities and return valid host', () => {
|
|
203
|
+
const mixedServiceUrl = new ServiceUrl({
|
|
204
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
205
|
+
hosts: [
|
|
206
|
+
{
|
|
207
|
+
host: 'example-host-neg.com',
|
|
208
|
+
priority: -1,
|
|
209
|
+
ttl: -1,
|
|
210
|
+
id: '1',
|
|
211
|
+
homeCluster: true,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
host: 'example-host-valid.com',
|
|
215
|
+
priority: 5,
|
|
216
|
+
ttl: -1,
|
|
217
|
+
id: '2',
|
|
218
|
+
homeCluster: true,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
name: 'mixed-priority-test',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = mixedServiceUrl._getPriorityHostUrl();
|
|
225
|
+
|
|
226
|
+
assert.include(result, 'example-host-valid.com');
|
|
227
|
+
assert.notInclude(result, 'example-host-neg.com');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should select lowest positive priority host when mixed with negative priorities', () => {
|
|
231
|
+
const mixedServiceUrl = new ServiceUrl({
|
|
232
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
233
|
+
hosts: [
|
|
234
|
+
{
|
|
235
|
+
host: 'example-host-neg.com',
|
|
236
|
+
priority: -1,
|
|
237
|
+
ttl: -1,
|
|
238
|
+
id: '1',
|
|
239
|
+
homeCluster: true,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
host: 'example-host-p5.com',
|
|
243
|
+
priority: 5,
|
|
244
|
+
ttl: -1,
|
|
245
|
+
id: '2',
|
|
246
|
+
homeCluster: true,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
host: 'example-host-p2.com',
|
|
250
|
+
priority: 2,
|
|
251
|
+
ttl: -1,
|
|
252
|
+
id: '3',
|
|
253
|
+
homeCluster: true,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
name: 'mixed-priority-test',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const result = mixedServiceUrl._getPriorityHostUrl();
|
|
260
|
+
|
|
261
|
+
assert.include(result, 'example-host-p2.com');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
154
264
|
});
|
|
155
265
|
|
|
156
266
|
describe('#failHost()', () => {
|