@webex/webex-core 3.12.0-next.5 → 3.12.0-next.50
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/README.md +5 -2
- package/dist/config.js +38 -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/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/catalog-url.js +87 -0
- package/dist/interceptors/catalog-url.js.map +1 -0
- package/dist/interceptors/payload-transformer.js +25 -4
- package/dist/interceptors/payload-transformer.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 +103 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/domains.js +90 -0
- package/dist/lib/domains.js.map +1 -0
- package/dist/lib/services/service-catalog.js +108 -18
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +232 -34
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +14 -13
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +241 -37
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +11 -3
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +42 -0
- package/src/credentials-config.js +13 -0
- package/src/index.js +1 -0
- package/src/interceptors/catalog-url.js +66 -0
- package/src/interceptors/payload-transformer.js +23 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +93 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +112 -16
- package/src/lib/services/services.js +208 -20
- package/src/lib/services-v2/service-catalog.ts +16 -12
- package/src/lib/services-v2/services-v2.ts +218 -21
- package/src/webex-core.js +8 -0
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/services.js +183 -112
- package/test/integration/spec/services-v2/services-v2.js +169 -98
- package/test/unit/spec/credentials/credentials.js +247 -3
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/interceptors/catalog-url.js +224 -0
- package/test/unit/spec/interceptors/payload-transformer.js +119 -1
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +368 -11
- package/test/unit/spec/services/services.js +440 -2
- package/test/unit/spec/services-v2/service-catalog.ts +134 -11
- package/test/unit/spec/services-v2/services-v2.ts +467 -0
- package/test/unit/spec/webex-core.js +77 -1
|
@@ -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', () => {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chai from 'chai';
|
|
6
|
+
import chaiAsPromised from 'chai-as-promised';
|
|
7
|
+
import sinon from 'sinon';
|
|
8
|
+
import {CatalogUrlInterceptor} from '@webex/webex-core';
|
|
9
|
+
|
|
10
|
+
const {assert} = chai;
|
|
11
|
+
|
|
12
|
+
chai.use(chaiAsPromised);
|
|
13
|
+
sinon.assert.expose(chai.assert, {prefix: ''});
|
|
14
|
+
|
|
15
|
+
describe('webex-core', () => {
|
|
16
|
+
describe('Interceptors', () => {
|
|
17
|
+
describe('CatalogUrlInterceptor', () => {
|
|
18
|
+
let interceptor;
|
|
19
|
+
let webex;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
webex = {
|
|
23
|
+
config: {
|
|
24
|
+
services: {},
|
|
25
|
+
},
|
|
26
|
+
internal: {
|
|
27
|
+
services: {
|
|
28
|
+
getServiceFromUrl: sinon.stub(),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
logger: {
|
|
32
|
+
warn: sinon.stub(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
interceptor = Reflect.apply(CatalogUrlInterceptor.create, webex, []);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('#onRequest()', () => {
|
|
40
|
+
it('allows catalog URLs', async () => {
|
|
41
|
+
const options = {uri: 'https://conv-a.wbx2.com/conversation/api/v1/messages'};
|
|
42
|
+
webex.internal.services.getServiceFromUrl.returns({name: 'conversation'});
|
|
43
|
+
|
|
44
|
+
const result = await interceptor.onRequest(options);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(result, options);
|
|
47
|
+
assert.calledOnceWithExactly(webex.internal.services.getServiceFromUrl, options.uri);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('blocks non-catalog URLs with error', async () => {
|
|
51
|
+
const options = {uri: 'https://evil.attacker.com/steal-data'};
|
|
52
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
53
|
+
|
|
54
|
+
await assert.isRejected(
|
|
55
|
+
interceptor.onRequest(options),
|
|
56
|
+
/Request blocked: URL not in service catalog/
|
|
57
|
+
);
|
|
58
|
+
assert.calledOnce(webex.internal.services.getServiceFromUrl);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('service parameter bypass', () => {
|
|
62
|
+
it('skips validation when service has not been resolved to a URL', async () => {
|
|
63
|
+
const options = {
|
|
64
|
+
service: 'conversation',
|
|
65
|
+
resource: '/messages',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await interceptor.onRequest(options);
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(result, options);
|
|
71
|
+
assert.notCalled(webex.internal.services.getServiceFromUrl);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('blocks a non-catalog URL when service is also present', async () => {
|
|
75
|
+
const options = {
|
|
76
|
+
service: 'conversation',
|
|
77
|
+
resource: '/messages',
|
|
78
|
+
uri: 'https://attacker.com/steal',
|
|
79
|
+
headers: {authorization: 'Bearer token'},
|
|
80
|
+
};
|
|
81
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
82
|
+
|
|
83
|
+
await assert.isRejected(interceptor.onRequest(options), /Request blocked/);
|
|
84
|
+
assert.calledOnceWithExactly(webex.internal.services.getServiceFromUrl, options.uri);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('url parameter support', () => {
|
|
89
|
+
it('validates options.url when options.uri is not present', async () => {
|
|
90
|
+
const options = {url: 'https://conv-a.wbx2.com/conversation/api/v1/messages'};
|
|
91
|
+
webex.internal.services.getServiceFromUrl.returns({name: 'conversation'});
|
|
92
|
+
|
|
93
|
+
const result = await interceptor.onRequest(options);
|
|
94
|
+
|
|
95
|
+
assert.deepEqual(result, options);
|
|
96
|
+
assert.calledOnceWithExactly(webex.internal.services.getServiceFromUrl, options.url);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('edge cases', () => {
|
|
101
|
+
it('allows request when no URL is present', async () => {
|
|
102
|
+
const options = {method: 'GET'};
|
|
103
|
+
|
|
104
|
+
const result = await interceptor.onRequest(options);
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(result, options);
|
|
107
|
+
assert.notCalled(webex.internal.services.getServiceFromUrl);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('handles missing services plugin gracefully', async () => {
|
|
111
|
+
webex.internal = undefined;
|
|
112
|
+
const options = {uri: 'https://example.com/api'};
|
|
113
|
+
|
|
114
|
+
const result = await interceptor.onRequest(options);
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(result, options);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('security scenarios', () => {
|
|
121
|
+
it('blocks Mercury-injected malicious URLs', async () => {
|
|
122
|
+
// This is the SSRF attack vector: Mercury event contains attacker URL
|
|
123
|
+
// that the flag plugin would POST to without validation
|
|
124
|
+
const options = {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
uri: 'https://attacker-controlled.com/exfiltrate',
|
|
127
|
+
body: {sensitiveData: 'secrets'},
|
|
128
|
+
};
|
|
129
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
130
|
+
|
|
131
|
+
await assert.isRejected(interceptor.onRequest(options), /Request blocked/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('allows legitimate activity service URLs', async () => {
|
|
135
|
+
const options = {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
uri: 'https://conv-a.wbx2.com/conversation/api/v1/activities/12345',
|
|
138
|
+
body: {verb: 'flag'},
|
|
139
|
+
};
|
|
140
|
+
webex.internal.services.getServiceFromUrl.returns({name: 'conversation'});
|
|
141
|
+
|
|
142
|
+
const result = await interceptor.onRequest(options);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(result, options);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('blocks URLs with similar-looking hostnames', async () => {
|
|
148
|
+
// Attacker might try to use a URL that looks similar to a catalog URL
|
|
149
|
+
const options = {
|
|
150
|
+
uri: 'https://conv-a.wbx2.com.attacker.com/api',
|
|
151
|
+
};
|
|
152
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
153
|
+
|
|
154
|
+
await assert.isRejected(interceptor.onRequest(options), /Request blocked/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('allowedDomains support', () => {
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
// Add allowedDomains methods to mock
|
|
161
|
+
webex.internal.services.validateDomains = true;
|
|
162
|
+
webex.internal.services.hasAllowedDomains = sinon.stub();
|
|
163
|
+
webex.internal.services.isAllowedDomainUrl = sinon.stub();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('allows URLs in allowedDomains when not in catalog', async () => {
|
|
167
|
+
const options = {uri: 'https://cdn.example.com/files/encrypted-attachment.bin'};
|
|
168
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
169
|
+
webex.internal.services.hasAllowedDomains.returns(true);
|
|
170
|
+
webex.internal.services.isAllowedDomainUrl.returns(true);
|
|
171
|
+
|
|
172
|
+
const result = await interceptor.onRequest(options);
|
|
173
|
+
|
|
174
|
+
assert.deepEqual(result, options);
|
|
175
|
+
assert.calledOnceWithExactly(webex.internal.services.isAllowedDomainUrl, options.uri);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('blocks URLs not in catalog and not in allowedDomains', async () => {
|
|
179
|
+
const options = {uri: 'https://attacker.com/steal'};
|
|
180
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
181
|
+
webex.internal.services.hasAllowedDomains.returns(true);
|
|
182
|
+
webex.internal.services.isAllowedDomainUrl.returns(false);
|
|
183
|
+
|
|
184
|
+
await assert.isRejected(
|
|
185
|
+
interceptor.onRequest(options),
|
|
186
|
+
/Request blocked: URL not in service catalog or allowed domains/
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('blocks when validateDomains is false', async () => {
|
|
191
|
+
const options = {uri: 'https://cdn.example.com/files/attachment.bin'};
|
|
192
|
+
webex.internal.services.validateDomains = false;
|
|
193
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
194
|
+
webex.internal.services.hasAllowedDomains.returns(true);
|
|
195
|
+
webex.internal.services.isAllowedDomainUrl.returns(true);
|
|
196
|
+
|
|
197
|
+
await assert.isRejected(interceptor.onRequest(options), /Request blocked/);
|
|
198
|
+
assert.notCalled(webex.internal.services.isAllowedDomainUrl);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('blocks when no allowedDomains configured', async () => {
|
|
202
|
+
const options = {uri: 'https://cdn.example.com/files/attachment.bin'};
|
|
203
|
+
webex.internal.services.getServiceFromUrl.returns(undefined);
|
|
204
|
+
webex.internal.services.hasAllowedDomains.returns(false);
|
|
205
|
+
|
|
206
|
+
await assert.isRejected(interceptor.onRequest(options), /Request blocked/);
|
|
207
|
+
assert.notCalled(webex.internal.services.isAllowedDomainUrl);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('skips allowedDomains check when URL is in catalog', async () => {
|
|
211
|
+
const options = {uri: 'https://conv-a.wbx2.com/conversation/api/v1/messages'};
|
|
212
|
+
webex.internal.services.getServiceFromUrl.returns({name: 'conversation'});
|
|
213
|
+
|
|
214
|
+
const result = await interceptor.onRequest(options);
|
|
215
|
+
|
|
216
|
+
assert.deepEqual(result, options);
|
|
217
|
+
assert.notCalled(webex.internal.services.hasAllowedDomains);
|
|
218
|
+
assert.notCalled(webex.internal.services.isAllowedDomainUrl);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import {assert} from '@webex/test-helper-chai';
|
|
6
6
|
import {capitalize} from 'lodash';
|
|
7
|
-
import
|
|
7
|
+
import sinon from 'sinon';
|
|
8
|
+
import WebexCore, {PayloadTransformerInterceptor} from '@webex/webex-core';
|
|
8
9
|
// TODO: fix circular dependency core->metrics->core https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-515520
|
|
9
10
|
require('@webex/internal-plugin-metrics');
|
|
10
11
|
|
|
@@ -153,5 +154,122 @@ describe('webex-core', () => {
|
|
|
153
154
|
));
|
|
154
155
|
});
|
|
155
156
|
});
|
|
157
|
+
|
|
158
|
+
describe('PayloadTransformerInterceptor repeated inbound transforms', () => {
|
|
159
|
+
const createInterceptor = (skipRepeatedInboundTransforms) => {
|
|
160
|
+
const transform = sinon.stub().callsFake((direction, value) => Promise.resolve(value));
|
|
161
|
+
const interceptor = new PayloadTransformerInterceptor({
|
|
162
|
+
webex: {
|
|
163
|
+
config: {payloadTransformer: {skipRepeatedInboundTransforms}},
|
|
164
|
+
transform,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {interceptor, transform};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
describe('#onResponse()', () => {
|
|
172
|
+
it('transforms the same response every time when skipping repeated transforms is disabled', async () => {
|
|
173
|
+
const {interceptor, transform} = createInterceptor(false);
|
|
174
|
+
const response = {};
|
|
175
|
+
|
|
176
|
+
await interceptor.onResponse({}, response);
|
|
177
|
+
await interceptor.onResponse({}, response);
|
|
178
|
+
|
|
179
|
+
assert.calledTwice(transform);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('transforms the same response only once when repeated transforms are skipped', async () => {
|
|
183
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
184
|
+
const response = {};
|
|
185
|
+
|
|
186
|
+
assert.equal(await interceptor.onResponse({}, response), response);
|
|
187
|
+
assert.equal(await interceptor.onResponse({}, response), response);
|
|
188
|
+
assert.calledOnceWithExactly(transform, 'inbound', response);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('shares an in-flight transform for concurrent calls with the same response', async () => {
|
|
192
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
193
|
+
const response = {};
|
|
194
|
+
let resolveTransform;
|
|
195
|
+
|
|
196
|
+
transform.callsFake(
|
|
197
|
+
(direction, value) =>
|
|
198
|
+
new Promise((resolve) => {
|
|
199
|
+
resolveTransform = () => resolve(value);
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const firstTransform = interceptor.onResponse({}, response);
|
|
204
|
+
const secondTransform = interceptor.onResponse({}, response);
|
|
205
|
+
|
|
206
|
+
assert.equal(firstTransform, secondTransform);
|
|
207
|
+
assert.calledOnceWithExactly(transform, 'inbound', response);
|
|
208
|
+
|
|
209
|
+
resolveTransform();
|
|
210
|
+
|
|
211
|
+
assert.equal(await firstTransform, response);
|
|
212
|
+
assert.equal(await secondTransform, response);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('transforms distinct response objects independently', async () => {
|
|
216
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
217
|
+
const firstResponse = {};
|
|
218
|
+
const secondResponse = {};
|
|
219
|
+
|
|
220
|
+
await interceptor.onResponse({}, firstResponse);
|
|
221
|
+
await interceptor.onResponse({}, secondResponse);
|
|
222
|
+
|
|
223
|
+
assert.calledTwice(transform);
|
|
224
|
+
assert.isTrue(transform.firstCall.calledWithExactly('inbound', firstResponse));
|
|
225
|
+
assert.isTrue(transform.secondCall.calledWithExactly('inbound', secondResponse));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('retries transformation after a failed transform', async () => {
|
|
229
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
230
|
+
const response = {};
|
|
231
|
+
|
|
232
|
+
transform.onFirstCall().rejects(new Error('transform failed'));
|
|
233
|
+
|
|
234
|
+
await assert.isRejected(interceptor.onResponse({}, response), 'transform failed');
|
|
235
|
+
assert.equal(await interceptor.onResponse({}, response), response);
|
|
236
|
+
assert.calledTwice(transform);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('stores transform state as non-enumerable response metadata', async () => {
|
|
240
|
+
const {interceptor} = createInterceptor(true);
|
|
241
|
+
const response = {body: {value: true}};
|
|
242
|
+
|
|
243
|
+
await interceptor.onResponse({}, response);
|
|
244
|
+
|
|
245
|
+
const [marker] = Object.getOwnPropertySymbols(response);
|
|
246
|
+
|
|
247
|
+
assert.isDefined(marker);
|
|
248
|
+
assert.isFalse(Object.getOwnPropertyDescriptor(response, marker).enumerable);
|
|
249
|
+
assert.deepEqual(JSON.parse(JSON.stringify(response)), {body: {value: true}});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not mark a response when transforms are disabled for that request', async () => {
|
|
253
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
254
|
+
const response = {};
|
|
255
|
+
|
|
256
|
+
assert.equal(await interceptor.onResponse({disableTransform: true}, response), response);
|
|
257
|
+
assert.equal(await interceptor.onResponse({}, response), response);
|
|
258
|
+
assert.calledOnceWithExactly(transform, 'inbound', response);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('#onResponseError()', () => {
|
|
263
|
+
it('continues transforming repeated error objects independently', async () => {
|
|
264
|
+
const {interceptor, transform} = createInterceptor(true);
|
|
265
|
+
const reason = new Error('request failed');
|
|
266
|
+
|
|
267
|
+
await assert.isRejected(interceptor.onResponseError({}, reason), 'request failed');
|
|
268
|
+
await assert.isRejected(interceptor.onResponseError({}, reason), 'request failed');
|
|
269
|
+
assert.calledTwice(transform);
|
|
270
|
+
assert.alwaysCalledWithExactly(transform, 'inbound', reason);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
156
274
|
});
|
|
157
275
|
});
|
|
@@ -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', () => {
|