UNPKG

@webex/webex-core

Version:

Plugin handling for Cisco Webex

331 lines (273 loc) • 10.2 kB
/*! * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ import FakeTimers from '@sinonjs/fake-timers'; import {assert} from '@webex/test-helper-chai'; import MockWebex from '@webex/test-helper-mock-webex'; import sinon from 'sinon'; import {Batcher} from '@webex/webex-core'; function promiseTick(count) { let promise = Promise.resolve(); while (count > 1) { promise = promise.then(() => promiseTick(1)); count -= 1; } return promise; } describe('webex-core', () => { describe('Batcher', () => { let webex; const MockBatcher = Batcher.extend({ namespace: 'mock', submitHttpRequest(payload) { return webex.request({ service: 'mock', resource: '/batch', body: payload, }); }, fingerprintRequest(req) { return Promise.resolve(req); }, fingerprintResponse(res) { return Promise.resolve(res); }, }); const OutOfBandBatcher = MockBatcher.extend({ handleHttpSuccess() { return Promise.resolve(); }, fingerprintRequest(req) { return Promise.resolve(req.id); }, fingerprintResponse(res) { return Promise.resolve(res.id); }, }); const BATCHER_MAX_CALLS = 10; const BATCHER_MAX_WAIT = 5; const BATCHER_WAIT = 2; beforeEach(() => { webex = new MockWebex({ children: { batcher: MockBatcher, outOfBandBatcher: OutOfBandBatcher, }, }); webex.config.mock = { batcherMaxCalls: BATCHER_MAX_CALLS, batcherWait: BATCHER_WAIT, batcherMaxWait: BATCHER_MAX_WAIT, }; }); let clock; beforeEach(() => { clock = FakeTimers.install({now: Date.now()}); }); afterEach(() => { clock.uninstall(); }); describe('#request()', () => { it('coalesces requests made in a short time period into a single request', () => { const promises = []; webex.request.returns(Promise.resolve({body: [0, 1, 2]})); promises.push(webex.internal.batcher.request(0)); assert.notCalled(webex.request); promises.push(webex.internal.batcher.request(1)); assert.notCalled(webex.request); promises.push(webex.internal.batcher.request(2)); assert.notCalled(webex.request); return promiseTick(50) .then(() => { clock.tick(1); assert.notCalled(webex.request); clock.tick(1); return promiseTick(50); }) .then(() => { assert.calledOnce(webex.request); return Promise.all(promises); }) .then((results) => { assert.lengthOf(results, 3); for (let i = 0; i < 3; i += 1) { assert.equal(results[i], i); } clock.tick(250); return promiseTick(50); }) .then(() => { assert.calledOnce(webex.request); }); }); // This test is mostly here to make sure that unexpected failures (e.g., // those cause by forgetting to wrap a return value in a Promise) don't // get squelched. it('propagates error from inside the call chain', () => { // This is way easier to prove if we don't need to control the clock clock.uninstall(); sinon .stub(webex.internal.batcher, 'fingerprintResponse') .throws(new Error('simulated failure')); webex.request.returns(Promise.resolve({body: [{id: 1}]})); return assert.isRejected(webex.internal.batcher.request({id: 1}), /simulated failure/); }); describe('when the http request fails', () => { it('fails the whole batch', () => { const p1 = webex.internal.batcher.request(1); const p2 = webex.internal.batcher.request(2); // eslint-disable-next-line prefer-promise-reject-errors webex.request.returns(Promise.reject({statusCode: 0})); return promiseTick(50) .then(() => clock.tick(2)) .then(() => promiseTick(50)) .then(() => { assert.calledOnce(webex.request); return Promise.all([assert.isRejected(p1), assert.isRejected(p2)]); }); }); }); describe('when the number of request attempts exceeds a given threshold', () => { it('executes the batch request, regardless of the time passed', () => { const result = []; webex.request.returns(Promise.resolve({body: result})); const promises = []; // eslint-disable-next-line no-unmodified-loop-condition for (let i = 0; i < BATCHER_MAX_CALLS; i += 1) { result.push(i); promises.push(webex.internal.batcher.request(i)); assert.notCalled(webex.request); } return promiseTick(50) .then(() => { assert.calledOnce(webex.request); return Promise.all(promises); }) .then((results) => { assert.lengthOf(results, BATCHER_MAX_CALLS); // eslint-disable-next-line no-unmodified-loop-condition for (let i = 0; i < BATCHER_MAX_CALLS; i += 1) { assert.equal(results[i], i); } clock.tick(250); return promiseTick(50); }) .then(() => { assert.calledOnce(webex.request); }); }); }); describe('when the requests are requested continuously', () => { describe('when a configured time period is exceeded', () => { it('executes the batch request', () => { const promises = []; const result = []; // eslint-disable-next-line no-unmodified-loop-condition for (let i = 0; i < BATCHER_MAX_WAIT + 1; i += 1) { result.push(i); } webex.request.returns(Promise.resolve({body: result})); webex.request.onCall(1).returns(Promise.resolve({body: []})); return result .reduce( (promise, i) => promise.then(() => { promises.push(webex.internal.batcher.request(i)); clock.tick(1); return promiseTick(50); }), Promise.resolve() ) .then(() => { assert.calledOnce(webex.request); return Promise.all(promises); }) .then((results) => { assert.lengthOf(results, BATCHER_MAX_WAIT + 1); // eslint-disable-next-line no-unmodified-loop-condition for (let i = 0; i < BATCHER_MAX_WAIT + 1; i += 1) { assert.equal(results[i], i); } clock.tick(250); return promiseTick(50); }) .then(() => { // This assertion is different than the other tests because // we've requestd enough requests that the normal debounce will // take over assert.calledTwice(webex.request); }); }); }); describe('when the request attempts exceeds a given threshold', () => { it('executes the requests for the amount of max calls', () => { const result = []; webex.request.returns(Promise.resolve({body: result})); const promises = []; // eslint-disable-next-line no-unmodified-loop-condition for (let i = 0; i < BATCHER_MAX_CALLS * 2; i += 1) { result.push(i); promises.push(webex.internal.batcher.request(i)); } return Promise.all(promises).then((results) => { assert.calledTwice(webex.request); assert.lengthOf(webex.request.args[0][0].body, BATCHER_MAX_CALLS); assert.lengthOf(webex.request.args[1][0].body, BATCHER_MAX_CALLS); assert.lengthOf(results, BATCHER_MAX_CALLS * 2); }); }); }); }); describe('when the same request is made twice before the first one completes', () => { it('returns the same result', () => { webex.request.returns(Promise.resolve({body: [1]})); const p1 = webex.internal.batcher.request(1); const p2 = webex.internal.batcher.request(1); const promise = Promise.all([p1, p2]); return promiseTick(50) .then(() => { clock.tick(2); return promiseTick(50); }) .then(() => { assert.calledOnce(webex.request); assert.deepEqual(webex.request.args[0][0], { service: 'mock', resource: '/batch', body: [1], }); return promise.then((result) => assert.deepEqual(result, [1, 1])); }); }); }); describe("when it's overridden to handle out-of-band responses", () => { it('resolves as expected', () => { sinon.spy(webex.internal.outOfBandBatcher, 'fingerprintResponse'); const promise = webex.internal.outOfBandBatcher.request({id: 1}); return promiseTick(50) .then(() => clock.tick(2)) .then(() => { assert.called(webex.request); assert.notCalled(webex.internal.outOfBandBatcher.fingerprintResponse); webex.internal.outOfBandBatcher.acceptItem({id: 1, data: 2}); return promiseTick(50); }) .then(() => promise) .then((res) => { assert.deepEqual(res, { id: 1, data: 2, }); }); }); }); }); describe('#handleHttpError()', () => { it('handles a non WebexHttpError object passed', () => assert.isRejected( webex.internal.batcher.handleHttpError('simulated failure'), /simulated failure/ )); }); }); });