diff --git a/packages/datafile-manager/__test__/backoffController.spec.ts b/packages/datafile-manager/__test__/backoffController.spec.ts index 851030c5d..c4aa378be 100644 --- a/packages/datafile-manager/__test__/backoffController.spec.ts +++ b/packages/datafile-manager/__test__/backoffController.spec.ts @@ -14,50 +14,50 @@ * limitations under the License. */ -import BackoffController from '../src/backoffController' +import BackoffController from '../src/backoffController'; describe('backoffController', () => { describe('getDelay', () => { it('returns 0 from getDelay if there have been no errors', () => { - const controller = new BackoffController() - expect(controller.getDelay()).toBe(0) - }) + const controller = new BackoffController(); + expect(controller.getDelay()).toBe(0); + }); it('increases the delay returned from getDelay (up to a maximum value) after each call to countError', () => { - const controller = new BackoffController() - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(8000) - expect(controller.getDelay()).toBeLessThan(9000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(16000) - expect(controller.getDelay()).toBeLessThan(17000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(32000) - expect(controller.getDelay()).toBeLessThan(33000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(64000) - expect(controller.getDelay()).toBeLessThan(65000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(128000) - expect(controller.getDelay()).toBeLessThan(129000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(256000) - expect(controller.getDelay()).toBeLessThan(257000) - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(512000) - expect(controller.getDelay()).toBeLessThan(513000) + const controller = new BackoffController(); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(8000); + expect(controller.getDelay()).toBeLessThan(9000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(16000); + expect(controller.getDelay()).toBeLessThan(17000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(32000); + expect(controller.getDelay()).toBeLessThan(33000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(64000); + expect(controller.getDelay()).toBeLessThan(65000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(128000); + expect(controller.getDelay()).toBeLessThan(129000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(256000); + expect(controller.getDelay()).toBeLessThan(257000); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(512000); + expect(controller.getDelay()).toBeLessThan(513000); // Maximum reached - additional errors should not increase the delay further - controller.countError() - expect(controller.getDelay()).toBeGreaterThanOrEqual(512000) - expect(controller.getDelay()).toBeLessThan(513000) - }) + controller.countError(); + expect(controller.getDelay()).toBeGreaterThanOrEqual(512000); + expect(controller.getDelay()).toBeLessThan(513000); + }); it('resets the error count when reset is called', () => { - const controller = new BackoffController() - controller.countError() - expect(controller.getDelay()).toBeGreaterThan(0) - controller.reset() - expect(controller.getDelay()).toBe(0) - }) - }) -}) + const controller = new BackoffController(); + controller.countError(); + expect(controller.getDelay()).toBeGreaterThan(0); + controller.reset(); + expect(controller.getDelay()).toBe(0); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/browserDatafileManager.spec.ts b/packages/datafile-manager/__test__/browserDatafileManager.spec.ts index d5f38c74c..5ea707900 100644 --- a/packages/datafile-manager/__test__/browserDatafileManager.spec.ts +++ b/packages/datafile-manager/__test__/browserDatafileManager.spec.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import BrowserDatafileManager from '../src/browserDatafileManager' -import * as browserRequest from '../src/browserRequest' -import { Headers, AbortableRequest } from '../src/http' +import BrowserDatafileManager from '../src/browserDatafileManager'; +import * as browserRequest from '../src/browserRequest'; +import { Headers, AbortableRequest } from '../src/http'; import { advanceTimersByTime, getTimerCount } from './testUtils'; describe('browserDatafileManager', () => { - let makeGetRequestSpy: jest.SpyInstance + let makeGetRequestSpy: jest.SpyInstance; beforeEach(() => { - jest.useFakeTimers() - makeGetRequestSpy = jest.spyOn(browserRequest, 'makeGetRequest') - }) + jest.useFakeTimers(); + makeGetRequestSpy = jest.spyOn(browserRequest, 'makeGetRequest'); + }); afterEach(() => { - jest.restoreAllMocks() - jest.clearAllTimers() - }) + jest.restoreAllMocks(); + jest.clearAllTimers(); + }); it('calls makeGetRequest when started', async () => { makeGetRequestSpy.mockReturnValue({ @@ -38,21 +38,21 @@ describe('browserDatafileManager', () => { statusCode: 200, body: '{"foo":"bar"}', headers: {}, - }) - }) + }), + }); const manager = new BrowserDatafileManager({ sdkKey: '1234', autoUpdate: false, - }) - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) - expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json') - expect(makeGetRequestSpy.mock.calls[0][1]).toEqual({}) + }); + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json'); + expect(makeGetRequestSpy.mock.calls[0][1]).toEqual({}); - await manager.onReady() - await manager.stop() - }) + await manager.onReady(); + await manager.stop(); + }); it('calls makeGetRequest for live update requests', async () => { makeGetRequestSpy.mockReturnValue({ @@ -63,23 +63,23 @@ describe('browserDatafileManager', () => { headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, - }) - }) + }), + }); const manager = new BrowserDatafileManager({ sdkKey: '1234', autoUpdate: true, - }) - manager.start() - await manager.onReady() - await advanceTimersByTime(300000) - expect(makeGetRequestSpy).toBeCalledTimes(2) - expect(makeGetRequestSpy.mock.calls[1][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json') + }); + manager.start(); + await manager.onReady(); + await advanceTimersByTime(300000); + expect(makeGetRequestSpy).toBeCalledTimes(2); + expect(makeGetRequestSpy.mock.calls[1][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json'); expect(makeGetRequestSpy.mock.calls[1][1]).toEqual({ - 'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT' - }) + 'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT', + }); - await manager.stop() - }) + await manager.stop(); + }); it('defaults to false for autoUpdate', async () => { makeGetRequestSpy.mockReturnValue({ @@ -90,16 +90,16 @@ describe('browserDatafileManager', () => { headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, - }) - }) + }), + }); const manager = new BrowserDatafileManager({ sdkKey: '1234', - }) - manager.start() - await manager.onReady() + }); + manager.start(); + await manager.onReady(); // Should not set a timeout for a later update - expect(getTimerCount()).toBe(0) + expect(getTimerCount()).toBe(0); - await manager.stop() - }) -}) + await manager.stop(); + }); +}); diff --git a/packages/datafile-manager/__test__/browserRequest.spec.ts b/packages/datafile-manager/__test__/browserRequest.spec.ts index 004d93af0..17c81ae13 100644 --- a/packages/datafile-manager/__test__/browserRequest.spec.ts +++ b/packages/datafile-manager/__test__/browserRequest.spec.ts @@ -17,100 +17,94 @@ * limitations under the License. */ -import { - FakeXMLHttpRequest, - FakeXMLHttpRequestStatic, - fakeXhr -} from 'nise' -import { makeGetRequest } from '../src/browserRequest' +import { FakeXMLHttpRequest, FakeXMLHttpRequestStatic, fakeXhr } from 'nise'; +import { makeGetRequest } from '../src/browserRequest'; describe('browserRequest', () => { describe('makeGetRequest', () => { - let mockXHR: FakeXMLHttpRequestStatic - let xhrs: FakeXMLHttpRequest[] + let mockXHR: FakeXMLHttpRequestStatic; + let xhrs: FakeXMLHttpRequest[]; beforeEach(() => { - xhrs = [] - mockXHR = fakeXhr.useFakeXMLHttpRequest() - mockXHR.onCreate = req => xhrs.push(req) - }) + xhrs = []; + mockXHR = fakeXhr.useFakeXMLHttpRequest(); + mockXHR.onCreate = (req): number => xhrs.push(req); + }); afterEach(() => { - mockXHR.restore() - }) + mockXHR.restore(); + }); it('makes a GET request to the argument URL', async () => { - const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) + const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); - expect(xhrs.length).toBe(1) - const xhr = xhrs[0] - const { url, method } = xhr + expect(xhrs.length).toBe(1); + const xhr = xhrs[0]; + const { url, method } = xhr; expect({ url, method }).toEqual({ url: 'https://cdn.optimizely.com/datafiles/123.json', method: 'GET', - }) + }); - xhr.respond(200, {}, '{"foo":"bar"}') + xhr.respond(200, {}, '{"foo":"bar"}'); - await req.responsePromise - }) + await req.responsePromise; + }); it('returns a 200 response back to its superclass', async () => { - const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) + const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); - const xhr = xhrs[0] - xhr.respond(200, {}, '{"foo":"bar"}') + const xhr = xhrs[0]; + xhr.respond(200, {}, '{"foo":"bar"}'); - const resp = await req.responsePromise + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, headers: {}, body: '{"foo":"bar"}', - }) - }) + }); + }); it('returns a 404 response back to its superclass', async () => { - const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) + const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); - const xhr = xhrs[0] - xhr.respond(404, {}, '') + const xhr = xhrs[0]; + xhr.respond(404, {}, ''); - const resp = await req.responsePromise + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 404, headers: {}, body: '', - }) - }) + }); + }); it('includes headers from the headers argument in the request', async () => { const req = makeGetRequest('https://cdn.optimizely.com/dataifles/123.json', { 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', - }) + }); - expect(xhrs.length).toBe(1) - expect(xhrs[0].requestHeaders['if-modified-since']).toBe( - 'Fri, 08 Mar 2019 18:57:18 GMT', - ) + expect(xhrs.length).toBe(1); + expect(xhrs[0].requestHeaders['if-modified-since']).toBe('Fri, 08 Mar 2019 18:57:18 GMT'); - xhrs[0].respond(404, {}, '') + xhrs[0].respond(404, {}, ''); - await req.responsePromise - }) + await req.responsePromise; + }); it('includes headers from the response in the eventual response in the return value', async () => { - const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) + const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); - const xhr = xhrs[0] + const xhr = xhrs[0]; xhr.respond( 200, { 'content-type': 'application/json', 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', }, - '{"foo":"bar"}', - ) + '{"foo":"bar"}' + ); - const resp = await req.responsePromise + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, body: '{"foo":"bar"}', @@ -118,21 +112,21 @@ describe('browserRequest', () => { 'content-type': 'application/json', 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', }, - }) - }) + }); + }); it('returns a rejected promise when there is a request error', async () => { - const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) - xhrs[0].error() - await expect(req.responsePromise).rejects.toThrow() - }) + const req = makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); + xhrs[0].error(); + await expect(req.responsePromise).rejects.toThrow(); + }); it('sets a timeout on the request object', () => { - const onCreateMock = jest.fn() - mockXHR.onCreate = onCreateMock - makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}) - expect(onCreateMock).toBeCalledTimes(1) - expect(onCreateMock.mock.calls[0][0].timeout).toBe(60000) - }) - }) -}) + const onCreateMock = jest.fn(); + mockXHR.onCreate = onCreateMock; + makeGetRequest('https://cdn.optimizely.com/datafiles/123.json', {}); + expect(onCreateMock).toBeCalledTimes(1); + expect(onCreateMock.mock.calls[0][0].timeout).toBe(60000); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/eventEmitter.spec.ts b/packages/datafile-manager/__test__/eventEmitter.spec.ts index b7d393b49..6292d8a89 100644 --- a/packages/datafile-manager/__test__/eventEmitter.spec.ts +++ b/packages/datafile-manager/__test__/eventEmitter.spec.ts @@ -14,103 +14,103 @@ * limitations under the License. */ -import EventEmitter from '../src/eventEmitter' +import EventEmitter from '../src/eventEmitter'; describe('event_emitter', () => { describe('on', () => { let emitter: EventEmitter; beforeEach(() => { - emitter = new EventEmitter() - }) + emitter = new EventEmitter(); + }); it('can add a listener for the update event', () => { - const listener = jest.fn() - emitter.on('update', listener) - emitter.emit('update', { datafile: 'abcd' }) - expect(listener).toBeCalledTimes(1) - }) + const listener = jest.fn(); + emitter.on('update', listener); + emitter.emit('update', { datafile: 'abcd' }); + expect(listener).toBeCalledTimes(1); + }); it('passes the argument from emit to the listener', () => { - const listener = jest.fn() - emitter.on('update', listener) - emitter.emit('update', { datafile: 'abcd' }) - expect(listener).toBeCalledWith({ datafile: 'abcd' }) - }) + const listener = jest.fn(); + emitter.on('update', listener); + emitter.emit('update', { datafile: 'abcd' }); + expect(listener).toBeCalledWith({ datafile: 'abcd' }); + }); it('returns a dispose function that removes the listener', () => { - const listener = jest.fn() - const disposer = emitter.on('update', listener) - disposer() - emitter.emit('update', { datafile: 'efgh' }) - expect(listener).toBeCalledTimes(0) - }) + const listener = jest.fn(); + const disposer = emitter.on('update', listener); + disposer(); + emitter.emit('update', { datafile: 'efgh' }); + expect(listener).toBeCalledTimes(0); + }); it('can add several listeners for the update event', () => { - const listener1 = jest.fn() - const listener2 = jest.fn() - const listener3 = jest.fn() - emitter.on('update', listener1) - emitter.on('update', listener2) - emitter.on('update', listener3) - emitter.emit('update', { datafile: 'abcd' }) - expect(listener1).toBeCalledTimes(1) - expect(listener2).toBeCalledTimes(1) - expect(listener3).toBeCalledTimes(1) - }) + const listener1 = jest.fn(); + const listener2 = jest.fn(); + const listener3 = jest.fn(); + emitter.on('update', listener1); + emitter.on('update', listener2); + emitter.on('update', listener3); + emitter.emit('update', { datafile: 'abcd' }); + expect(listener1).toBeCalledTimes(1); + expect(listener2).toBeCalledTimes(1); + expect(listener3).toBeCalledTimes(1); + }); it('can add several listeners and remove only some of them', () => { - const listener1 = jest.fn() - const listener2 = jest.fn() - const listener3 = jest.fn() - const disposer1 = emitter.on('update', listener1) - const disposer2 = emitter.on('update', listener2) - emitter.on('update', listener3) - emitter.emit('update', { datafile: 'abcd' }) - expect(listener1).toBeCalledTimes(1) - expect(listener2).toBeCalledTimes(1) - expect(listener3).toBeCalledTimes(1) - disposer1() - disposer2() - emitter.emit('update', { datafile: 'efgh' }) - expect(listener1).toBeCalledTimes(1) - expect(listener2).toBeCalledTimes(1) - expect(listener3).toBeCalledTimes(2) - }) + const listener1 = jest.fn(); + const listener2 = jest.fn(); + const listener3 = jest.fn(); + const disposer1 = emitter.on('update', listener1); + const disposer2 = emitter.on('update', listener2); + emitter.on('update', listener3); + emitter.emit('update', { datafile: 'abcd' }); + expect(listener1).toBeCalledTimes(1); + expect(listener2).toBeCalledTimes(1); + expect(listener3).toBeCalledTimes(1); + disposer1(); + disposer2(); + emitter.emit('update', { datafile: 'efgh' }); + expect(listener1).toBeCalledTimes(1); + expect(listener2).toBeCalledTimes(1); + expect(listener3).toBeCalledTimes(2); + }); it('can add listeners for different events and remove only some of them', () => { - const readyListener = jest.fn() - const updateListener = jest.fn() - const readyDisposer = emitter.on('ready', readyListener) - const updateDisposer = emitter.on('update', updateListener) - emitter.emit('ready') - expect(readyListener).toBeCalledTimes(1) - expect(updateListener).toBeCalledTimes(0) - emitter.emit('update', { datafile: 'abcd' }) - expect(readyListener).toBeCalledTimes(1) - expect(updateListener).toBeCalledTimes(1) - readyDisposer() - emitter.emit('ready') - expect(readyListener).toBeCalledTimes(1) - expect(updateListener).toBeCalledTimes(1) - emitter.emit('update', { datafile: 'efgh' }) - expect(readyListener).toBeCalledTimes(1) - expect(updateListener).toBeCalledTimes(2) - updateDisposer() - emitter.emit('update', { datafile: 'ijkl' }) - expect(readyListener).toBeCalledTimes(1) - expect(updateListener).toBeCalledTimes(2) - }) + const readyListener = jest.fn(); + const updateListener = jest.fn(); + const readyDisposer = emitter.on('ready', readyListener); + const updateDisposer = emitter.on('update', updateListener); + emitter.emit('ready'); + expect(readyListener).toBeCalledTimes(1); + expect(updateListener).toBeCalledTimes(0); + emitter.emit('update', { datafile: 'abcd' }); + expect(readyListener).toBeCalledTimes(1); + expect(updateListener).toBeCalledTimes(1); + readyDisposer(); + emitter.emit('ready'); + expect(readyListener).toBeCalledTimes(1); + expect(updateListener).toBeCalledTimes(1); + emitter.emit('update', { datafile: 'efgh' }); + expect(readyListener).toBeCalledTimes(1); + expect(updateListener).toBeCalledTimes(2); + updateDisposer(); + emitter.emit('update', { datafile: 'ijkl' }); + expect(readyListener).toBeCalledTimes(1); + expect(updateListener).toBeCalledTimes(2); + }); it('can remove all listeners', () => { - const readyListener = jest.fn() - const updateListener = jest.fn() - emitter.on('ready', readyListener) - emitter.on('update', updateListener) - emitter.removeAllListeners() - emitter.emit('update', { datafile: 'abcd' }) - emitter.emit('ready') - expect(readyListener).toBeCalledTimes(0) - expect(updateListener).toBeCalledTimes(0) - }) - }) -}) + const readyListener = jest.fn(); + const updateListener = jest.fn(); + emitter.on('ready', readyListener); + emitter.on('update', updateListener); + emitter.removeAllListeners(); + emitter.emit('update', { datafile: 'abcd' }); + emitter.emit('ready'); + expect(readyListener).toBeCalledTimes(0); + expect(updateListener).toBeCalledTimes(0); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts b/packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts index 213ae025e..d96b2f74a 100644 --- a/packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts +++ b/packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts @@ -14,197 +14,199 @@ * limitations under the License. */ -import HttpPollingDatafileManager from '../src/httpPollingDatafileManager' -import { Headers, AbortableRequest, Response } from '../src/http' +import HttpPollingDatafileManager from '../src/httpPollingDatafileManager'; +import { Headers, AbortableRequest, Response } from '../src/http'; import { DatafileManagerConfig } from '../src/datafileManager'; -import { advanceTimersByTime, getTimerCount } from './testUtils' -import PersistentKeyValueCache from '../src/persistentKeyValueCache' +import { advanceTimersByTime, getTimerCount } from './testUtils'; +import PersistentKeyValueCache from '../src/persistentKeyValueCache'; jest.mock('../src/backoffController', () => { return jest.fn().mockImplementation(() => { - const getDelayMock = jest.fn().mockImplementation(() => 0) + const getDelayMock = jest.fn().mockImplementation(() => 0); return { getDelay: getDelayMock, countError: jest.fn(), reset: jest.fn(), - } - }) + }; + }); }); -import BackoffController from '../src/backoffController' +import BackoffController from '../src/backoffController'; // Test implementation: // - Does not make any real requests: just resolves with queued responses (tests push onto queuedResponses) class TestDatafileManager extends HttpPollingDatafileManager { - queuedResponses: (Response | Error)[] = [] + queuedResponses: (Response | Error)[] = []; - responsePromises: Promise[] = [] + responsePromises: Promise[] = []; - simulateResponseDelay: boolean = false + simulateResponseDelay = false; + // Need these unsued vars for the mock call types to work (being able to check calls) + // eslint-disable-next-line @typescript-eslint/no-unused-vars makeGetRequest(url: string, headers: Headers): AbortableRequest { - const nextResponse: Error | Response | undefined = this.queuedResponses.pop() - let responsePromise: Promise + const nextResponse: Error | Response | undefined = this.queuedResponses.pop(); + let responsePromise: Promise; if (nextResponse === undefined) { - responsePromise = Promise.reject('No responses queued') + responsePromise = Promise.reject('No responses queued'); } else if (nextResponse instanceof Error) { - responsePromise = Promise.reject(nextResponse) + responsePromise = Promise.reject(nextResponse); } else { if (this.simulateResponseDelay) { // Actual response will have some delay. This is required to get expected behavior for caching. - responsePromise = new Promise((resolve) => setTimeout(() => resolve(nextResponse), 50)) + responsePromise = new Promise(resolve => setTimeout(() => resolve(nextResponse), 50)); } else { - responsePromise = Promise.resolve(nextResponse) + responsePromise = Promise.resolve(nextResponse); } } - this.responsePromises.push(responsePromise) - return { responsePromise, abort: jest.fn() } + this.responsePromises.push(responsePromise); + return { responsePromise, abort: jest.fn() }; } getConfigDefaults(): Partial { - return {} + return {}; } } -const testCache : PersistentKeyValueCache = { +const testCache: PersistentKeyValueCache = { get(key: string): Promise { - let val = null - switch(key) { + let val = null; + switch (key) { case 'opt-datafile-keyThatExists': - val = { name: 'keyThatExists' } - break + val = { name: 'keyThatExists' }; + break; } - return Promise.resolve(val) + return Promise.resolve(val); }, - set(key: string, val: any): Promise { - return Promise.resolve() + set(): Promise { + return Promise.resolve(); }, - contains(key: string): Promise { - return Promise.resolve(false) + contains(): Promise { + return Promise.resolve(false); }, - remove(key: string): Promise { - return Promise.resolve() - } -} + remove(): Promise { + return Promise.resolve(); + }, +}; describe('httpPollingDatafileManager', () => { beforeEach(() => { - jest.useFakeTimers() - }) + jest.useFakeTimers(); + }); - let manager: TestDatafileManager + let manager: TestDatafileManager; afterEach(async () => { if (manager) { - manager.stop() + manager.stop(); } - jest.clearAllMocks() - jest.restoreAllMocks() - jest.clearAllTimers() - }) + jest.clearAllMocks(); + jest.restoreAllMocks(); + jest.clearAllTimers(); + }); describe('when constructed with sdkKey and datafile and autoUpdate: true,', () => { beforeEach(() => { - manager = new TestDatafileManager({ datafile: { foo: 'abcd' }, sdkKey: '123', autoUpdate: true }) - }) + manager = new TestDatafileManager({ datafile: { foo: 'abcd' }, sdkKey: '123', autoUpdate: true }); + }); it('returns the passed datafile from get', () => { - expect(manager.get()).toEqual({ foo: 'abcd' }) - }) + expect(manager.get()).toEqual({ foo: 'abcd' }); + }); it('after being started, fetches the datafile, updates itself, and updates itself again after a timeout', async () => { manager.queuedResponses.push( { statusCode: 200, body: '{"fooz": "barz"}', - headers: {} + headers: {}, }, { statusCode: 200, body: '{"foo": "bar"}', - headers: {} + headers: {}, } - ) - const updateFn = jest.fn() - manager.on('update', updateFn) - manager.start() - expect(manager.responsePromises.length).toBe(1) - await manager.responsePromises[0] - expect(manager.get()).toEqual({ foo: 'bar' }) - updateFn.mockReset() - - await advanceTimersByTime(300000) - - expect(manager.responsePromises.length).toBe(2) - await manager.responsePromises[1] - expect(updateFn).toBeCalledTimes(1) + ); + const updateFn = jest.fn(); + manager.on('update', updateFn); + manager.start(); + expect(manager.responsePromises.length).toBe(1); + await manager.responsePromises[0]; + expect(manager.get()).toEqual({ foo: 'bar' }); + updateFn.mockReset(); + + await advanceTimersByTime(300000); + + expect(manager.responsePromises.length).toBe(2); + await manager.responsePromises[1]; + expect(updateFn).toBeCalledTimes(1); expect(updateFn).toBeCalledWith({ - datafile: { fooz: 'barz' } - }) - expect(manager.get()).toEqual({ fooz: 'barz' }) - }) - }) + datafile: { fooz: 'barz' }, + }); + expect(manager.get()).toEqual({ fooz: 'barz' }); + }); + }); describe('when constructed with sdkKey and datafile and autoUpdate: false,', () => { beforeEach(() => { - manager = new TestDatafileManager({ datafile: { foo: 'abcd' }, sdkKey: '123', autoUpdate: false }) - }) + manager = new TestDatafileManager({ datafile: { foo: 'abcd' }, sdkKey: '123', autoUpdate: false }); + }); it('returns the passed datafile from get', () => { - expect(manager.get()).toEqual({ foo: 'abcd' }) - }) + expect(manager.get()).toEqual({ foo: 'abcd' }); + }); it('after being started, fetches the datafile, updates itself once, but does not schedule a future update', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', - headers: {} - }) - manager.start() - expect(manager.responsePromises.length).toBe(1) - await manager.responsePromises[0] - expect(manager.get()).toEqual({ foo: 'bar' }) - expect(getTimerCount()).toBe(0) - }) - }) + headers: {}, + }); + manager.start(); + expect(manager.responsePromises.length).toBe(1); + await manager.responsePromises[0]; + expect(manager.get()).toEqual({ foo: 'bar' }); + expect(getTimerCount()).toBe(0); + }); + }); describe('when constructed with sdkKey and autoUpdate: true', () => { beforeEach(() => { - manager = new TestDatafileManager({ sdkKey: '123', updateInterval: 1000, autoUpdate: true }) - }) + manager = new TestDatafileManager({ sdkKey: '123', updateInterval: 1000, autoUpdate: true }); + }); describe('initial state', () => { it('returns null from get before becoming ready', () => { - expect(manager.get()).toBeNull() - }) - }) + expect(manager.get()).toBeNull(); + }); + }); describe('started state', () => { it('passes the default datafile URL to the makeGetRequest method', async () => { - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) - expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/123.json') - await manager.onReady() - }) + }); + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/123.json'); + await manager.onReady(); + }); it('after being started, fetches the datafile and resolves onReady', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + }); + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); + }); it('does not update if the response body is not valid json', async () => { manager.queuedResponses.push( @@ -217,15 +219,15 @@ describe('httpPollingDatafileManager', () => { statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }, - ) - manager.start() - await manager.onReady() - await advanceTimersByTime(1000) - expect(manager.responsePromises.length).toBe(2) - await manager.responsePromises[1] - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + } + ); + manager.start(); + await manager.onReady(); + await advanceTimersByTime(1000); + expect(manager.responsePromises.length).toBe(2); + await manager.responsePromises[1]; + expect(manager.get()).toEqual({ foo: 'bar' }); + }); describe('live updates', () => { it('sets a timeout to update again after the update interval', async () => { @@ -239,15 +241,15 @@ describe('httpPollingDatafileManager', () => { statusCode: 200, body: '{"foo4": "bar4"}', headers: {}, - }, - ) - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) - await manager.responsePromises[0] - await advanceTimersByTime(1000) - expect(makeGetRequestSpy).toBeCalledTimes(2) - }) + } + ); + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + await manager.responsePromises[0]; + await advanceTimersByTime(1000); + expect(makeGetRequestSpy).toBeCalledTimes(2); + }); it('emits update events after live updates', async () => { manager.queuedResponses.push( @@ -265,75 +267,73 @@ describe('httpPollingDatafileManager', () => { statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }, - ) + } + ); - const updateFn = jest.fn() - manager.on('update', updateFn) + const updateFn = jest.fn(); + manager.on('update', updateFn); - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) - expect(updateFn).toBeCalledTimes(0) + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); + expect(updateFn).toBeCalledTimes(0); - await advanceTimersByTime(1000) - await manager.responsePromises[1] - expect(updateFn).toBeCalledTimes(1) - expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo2: 'bar2' } }) - expect(manager.get()).toEqual({ foo2: 'bar2' }) + await advanceTimersByTime(1000); + await manager.responsePromises[1]; + expect(updateFn).toBeCalledTimes(1); + expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo2: 'bar2' } }); + expect(manager.get()).toEqual({ foo2: 'bar2' }); - updateFn.mockReset() + updateFn.mockReset(); - await advanceTimersByTime(1000) - await manager.responsePromises[2] - expect(updateFn).toBeCalledTimes(1) - expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo3: 'bar3' } }) - expect(manager.get()).toEqual({ foo3: 'bar3' }) - }) + await advanceTimersByTime(1000); + await manager.responsePromises[2]; + expect(updateFn).toBeCalledTimes(1); + expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo3: 'bar3' } }); + expect(manager.get()).toEqual({ foo3: 'bar3' }); + }); describe('when the update interval time fires before the request is complete', () => { it('waits until the request is complete before making the next request', async () => { - let resolveResponsePromise: (resp: Response) => void + let resolveResponsePromise: (resp: Response) => void; const responsePromise: Promise = new Promise(res => { - resolveResponsePromise = res - }) + resolveResponsePromise = res; + }); const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest').mockReturnValueOnce({ abort() {}, responsePromise, - }) + }); - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); - await advanceTimersByTime(1000) - expect(makeGetRequestSpy).toBeCalledTimes(1) + await advanceTimersByTime(1000); + expect(makeGetRequestSpy).toBeCalledTimes(1); resolveResponsePromise!({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - await responsePromise - expect(makeGetRequestSpy).toBeCalledTimes(2) - }) - }) + }); + await responsePromise; + expect(makeGetRequestSpy).toBeCalledTimes(2); + }); + }); it('cancels a pending timeout when stop is called', async () => { - manager.queuedResponses.push( - { - statusCode: 200, - body: '{"foo": "bar"}', - headers: {}, - }, - ) + manager.queuedResponses.push({ + statusCode: 200, + body: '{"foo": "bar"}', + headers: {}, + }); - manager.start() - await manager.onReady() + manager.start(); + await manager.onReady(); - expect(getTimerCount()).toBe(1) - manager.stop() - expect(getTimerCount()).toBe(0) - }) + expect(getTimerCount()).toBe(1); + manager.stop(); + expect(getTimerCount()).toBe(0); + }); it('cancels reactions to a pending fetch when stop is called', async () => { manager.queuedResponses.push( @@ -346,38 +346,36 @@ describe('httpPollingDatafileManager', () => { statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }, - ) + } + ); - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); - advanceTimersByTime(1000) + advanceTimersByTime(1000); - expect(manager.responsePromises.length).toBe(2) - manager.stop() - await manager.responsePromises[1] + expect(manager.responsePromises.length).toBe(2); + manager.stop(); + await manager.responsePromises[1]; // Should not have updated datafile since manager was stopped - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + expect(manager.get()).toEqual({ foo: 'bar' }); + }); it('calls abort on the current request if there is a current request when stop is called', async () => { - manager.queuedResponses.push( - { - statusCode: 200, - body: '{"foo2": "bar2"}', - headers: {}, - } - ) - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') - manager.start() - const currentRequest = makeGetRequestSpy.mock.results[0] - expect(currentRequest.type).toBe('return') - expect(currentRequest.value.abort).toBeCalledTimes(0) - manager.stop() - expect(currentRequest.value.abort).toBeCalledTimes(1) - }) + manager.queuedResponses.push({ + statusCode: 200, + body: '{"foo2": "bar2"}', + headers: {}, + }); + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); + manager.start(); + const currentRequest = makeGetRequestSpy.mock.results[0]; + expect(currentRequest.type).toBe('return'); + expect(currentRequest.value.abort).toBeCalledTimes(0); + manager.stop(); + expect(currentRequest.value.abort).toBeCalledTimes(1); + }); it('can fail to become ready on the initial request, but succeed after a later polling update', async () => { manager.queuedResponses.push( @@ -389,20 +387,20 @@ describe('httpPollingDatafileManager', () => { { statusCode: 404, body: '', - headers: {} + headers: {}, } - ) + ); - manager.start() - expect(manager.responsePromises.length).toBe(1) - await manager.responsePromises[0] + manager.start(); + expect(manager.responsePromises.length).toBe(1); + await manager.responsePromises[0]; // Not ready yet due to first request failed, but should have queued a live update - expect(getTimerCount()).toBe(1) + expect(getTimerCount()).toBe(1); // Trigger the update, should fetch the next response which should succeed, then we get ready - advanceTimersByTime(1000) - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + advanceTimersByTime(1000); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); + }); describe('newness checking', () => { it('does not update if the response status is 304', async () => { @@ -419,25 +417,25 @@ describe('httpPollingDatafileManager', () => { 'Last-Modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, } - ) + ); - const updateFn = jest.fn() - manager.on('update', updateFn) + const updateFn = jest.fn(); + manager.on('update', updateFn); - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); // First response promise was for the initial 200 response - expect(manager.responsePromises.length).toBe(1) + expect(manager.responsePromises.length).toBe(1); // Trigger the queued update - advanceTimersByTime(1000) + advanceTimersByTime(1000); // Second response promise is for the 304 response - expect(manager.responsePromises.length).toBe(2) - await manager.responsePromises[1] + expect(manager.responsePromises.length).toBe(2); + await manager.responsePromises[1]; // Since the response was 304, updateFn should not have been called - expect(updateFn).toBeCalledTimes(0) - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + expect(updateFn).toBeCalledTimes(0); + expect(manager.get()).toEqual({ foo: 'bar' }); + }); it('sends if-modified-since using the last observed response last-modified', async () => { manager.queuedResponses.push( @@ -453,133 +451,129 @@ describe('httpPollingDatafileManager', () => { 'Last-Modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, } - ) - manager.start() - await manager.onReady() - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') - advanceTimersByTime(1000) - expect(makeGetRequestSpy).toBeCalledTimes(1) - const firstCall = makeGetRequestSpy.mock.calls[0] - const headers = firstCall[1] + ); + manager.start(); + await manager.onReady(); + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); + advanceTimersByTime(1000); + expect(makeGetRequestSpy).toBeCalledTimes(1); + const firstCall = makeGetRequestSpy.mock.calls[0]; + const headers = firstCall[1]; expect(headers).toEqual({ 'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT', - }) - }) - }) + }); + }); + }); describe('backoff', () => { it('uses the delay from the backoff controller getDelay method when greater than updateInterval', async () => { - const BackoffControllerMock = (BackoffController as unknown) as jest.Mock - const getDelayMock = BackoffControllerMock.mock.results[0].value.getDelay - getDelayMock.mockImplementationOnce(() => 5432) + const BackoffControllerMock = (BackoffController as unknown) as jest.Mock; + const getDelayMock = BackoffControllerMock.mock.results[0].value.getDelay; + getDelayMock.mockImplementationOnce(() => 5432); - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); - manager.queuedResponses.push( - { - statusCode: 404, - body: '', - headers: {} - } - ) - manager.start() - await manager.responsePromises[0] - expect(makeGetRequestSpy).toBeCalledTimes(1) + manager.queuedResponses.push({ + statusCode: 404, + body: '', + headers: {}, + }); + manager.start(); + await manager.responsePromises[0]; + expect(makeGetRequestSpy).toBeCalledTimes(1); // Should not make another request after 1 second because the error should have triggered backoff - advanceTimersByTime(1000) - expect(makeGetRequestSpy).toBeCalledTimes(1) + advanceTimersByTime(1000); + expect(makeGetRequestSpy).toBeCalledTimes(1); // But after another 5 seconds, another request should be made - advanceTimersByTime(5000) - expect(makeGetRequestSpy).toBeCalledTimes(2) - }) + advanceTimersByTime(5000); + expect(makeGetRequestSpy).toBeCalledTimes(2); + }); it('calls countError on the backoff controller when a non-success status code response is received', async () => { - manager.queuedResponses.push( - { - statusCode: 404, - body: '', - headers: {} - } - ) - manager.start() - await manager.responsePromises[0] - const BackoffControllerMock = (BackoffController as unknown) as jest.Mock - expect(BackoffControllerMock.mock.results[0].value.countError).toBeCalledTimes(1) - }) + manager.queuedResponses.push({ + statusCode: 404, + body: '', + headers: {}, + }); + manager.start(); + await manager.responsePromises[0]; + const BackoffControllerMock = (BackoffController as unknown) as jest.Mock; + expect(BackoffControllerMock.mock.results[0].value.countError).toBeCalledTimes(1); + }); it('calls countError on the backoff controller when the response promise rejects', async () => { - manager.queuedResponses.push(new Error('Connection failed')) - manager.start() + manager.queuedResponses.push(new Error('Connection failed')); + manager.start(); try { - await manager.responsePromises[0] + await manager.responsePromises[0]; } catch (e) { + //empty } - const BackoffControllerMock = (BackoffController as unknown) as jest.Mock - expect(BackoffControllerMock.mock.results[0].value.countError).toBeCalledTimes(1) - }) + const BackoffControllerMock = (BackoffController as unknown) as jest.Mock; + expect(BackoffControllerMock.mock.results[0].value.countError).toBeCalledTimes(1); + }); it('calls reset on the backoff controller when a success status code response is received', async () => { - manager.queuedResponses.push( - { - statusCode: 200, - body: '{"foo": "bar"}', - headers: { - 'Last-Modified': 'Fri, 08 Mar 2019 18:57:17 GMT', - }, - } - ) - manager.start() - const BackoffControllerMock = (BackoffController as unknown) as jest.Mock + manager.queuedResponses.push({ + statusCode: 200, + body: '{"foo": "bar"}', + headers: { + 'Last-Modified': 'Fri, 08 Mar 2019 18:57:17 GMT', + }, + }); + manager.start(); + const BackoffControllerMock = (BackoffController as unknown) as jest.Mock; // Reset is called in start - we want to check that it is also called after the response, so reset the mock here - BackoffControllerMock.mock.results[0].value.reset.mockReset() - await manager.onReady() - expect(BackoffControllerMock.mock.results[0].value.reset).toBeCalledTimes(1) - }) + BackoffControllerMock.mock.results[0].value.reset.mockReset(); + await manager.onReady(); + expect(BackoffControllerMock.mock.results[0].value.reset).toBeCalledTimes(1); + }); it('resets the backoff controller when start is called', async () => { - const BackoffControllerMock = (BackoffController as unknown) as jest.Mock - manager.start() - expect(BackoffControllerMock.mock.results[0].value.reset).toBeCalledTimes(1) + const BackoffControllerMock = (BackoffController as unknown) as jest.Mock; + manager.start(); + expect(BackoffControllerMock.mock.results[0].value.reset).toBeCalledTimes(1); try { - await manager.responsePromises[0] + await manager.responsePromises[0]; } catch (e) { + // empty } - }) - }) - }) - }) - }) + }); + }); + }); + }); + }); describe('when constructed with sdkKey and autoUpdate: false', () => { beforeEach(() => { - manager = new TestDatafileManager({ sdkKey: '123', autoUpdate: false }) - }) + manager = new TestDatafileManager({ sdkKey: '123', autoUpdate: false }); + }); it('after being started, fetches the datafile and resolves onReady', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) - }) + }); + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); + }); it('does not schedule a live update after ready', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - const updateFn = jest.fn() - manager.on('update', updateFn) - manager.start() - await manager.onReady() - expect(getTimerCount()).toBe(0) - }) + }); + const updateFn = jest.fn(); + manager.on('update', updateFn); + manager.start(); + await manager.onReady(); + expect(getTimerCount()).toBe(0); + }); // TODO: figure out what's wrong with this test it.skip('rejects the onReady promise if the initial request promise rejects', async () => { @@ -587,18 +581,21 @@ describe('httpPollingDatafileManager', () => { statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - manager.makeGetRequest = () => ({ abort() {}, responsePromise: Promise.reject(new Error('Could not connect')) }) - manager.start() - let didReject = false + }); + manager.makeGetRequest = (): AbortableRequest => ({ + abort(): void {}, + responsePromise: Promise.reject(new Error('Could not connect')), + }); + manager.start(); + let didReject = false; try { - await manager.onReady() + await manager.onReady(); } catch (e) { - didReject = true + didReject = true; } - expect(didReject).toBe(true) - }) - }) + expect(didReject).toBe(true); + }); + }); describe('when constructed with sdkKey and a valid urlTemplate', () => { beforeEach(() => { @@ -606,126 +603,126 @@ describe('httpPollingDatafileManager', () => { sdkKey: '456', updateInterval: 1000, urlTemplate: 'https://localhost:5556/datafiles/%s', - }) - }) + }); + }); it('uses the urlTemplate to create the url passed to the makeGetRequest method', async () => { - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', headers: {}, - }) - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) - expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://localhost:5556/datafiles/456') - await manager.onReady() - }) - }) + }); + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://localhost:5556/datafiles/456'); + await manager.onReady(); + }); + }); describe('when constructed with an update interval below the minimum', () => { beforeEach(() => { - manager = new TestDatafileManager({ sdkKey: '123', updateInterval: 500, autoUpdate: true }) - }) + manager = new TestDatafileManager({ sdkKey: '123', updateInterval: 500, autoUpdate: true }); + }); it('uses the default update interval', async () => { - const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest') + const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest'); manager.queuedResponses.push({ statusCode: 200, body: '{"foo3": "bar3"}', headers: {}, - }) + }); - manager.start() - await manager.onReady() - expect(makeGetRequestSpy).toBeCalledTimes(1) - await advanceTimersByTime(300000) - expect(makeGetRequestSpy).toBeCalledTimes(2) - }) - }) + manager.start(); + await manager.onReady(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + await advanceTimersByTime(300000); + expect(makeGetRequestSpy).toBeCalledTimes(2); + }); + }); describe('when constructed with a cache implementation having an already cached datafile', () => { beforeEach(() => { - manager = new TestDatafileManager({ + manager = new TestDatafileManager({ sdkKey: 'keyThatExists', updateInterval: 500, autoUpdate: true, cache: testCache, - }) - manager.simulateResponseDelay = true - }) + }); + manager.simulateResponseDelay = true; + }); it('uses cached version of datafile first and resolves the promise while network throws error and no update event is triggered', async () => { - manager.queuedResponses.push(new Error('Connection Error')) - const updateFn = jest.fn() - manager.on('update', updateFn) - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({name: 'keyThatExists'}) - await advanceTimersByTime(50) - expect(manager.get()).toEqual({name: 'keyThatExists'}) - expect(updateFn).toBeCalledTimes(0) - }) - - it('uses cached datafile, resolves ready promise, fetches new datafile from network and triggers update event', async() => { + manager.queuedResponses.push(new Error('Connection Error')); + const updateFn = jest.fn(); + manager.on('update', updateFn); + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ name: 'keyThatExists' }); + await advanceTimersByTime(50); + expect(manager.get()).toEqual({ name: 'keyThatExists' }); + expect(updateFn).toBeCalledTimes(0); + }); + + it('uses cached datafile, resolves ready promise, fetches new datafile from network and triggers update event', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', - headers: {} - }) - - const updateFn = jest.fn() - manager.on('update', updateFn) - manager.start() - await manager.onReady() - expect(manager.get()).toEqual({ name: 'keyThatExists' }) - expect(updateFn).toBeCalledTimes(0) - await advanceTimersByTime(50) - expect(manager.get()).toEqual({ foo: 'bar' }) - expect(updateFn).toBeCalledTimes(1) - }) - - it('sets newly recieved datafile in to cache', async() => { - const cacheSetSpy = jest.spyOn(testCache, 'set') + headers: {}, + }); + + const updateFn = jest.fn(); + manager.on('update', updateFn); + manager.start(); + await manager.onReady(); + expect(manager.get()).toEqual({ name: 'keyThatExists' }); + expect(updateFn).toBeCalledTimes(0); + await advanceTimersByTime(50); + expect(manager.get()).toEqual({ foo: 'bar' }); + expect(updateFn).toBeCalledTimes(1); + }); + + it('sets newly recieved datafile in to cache', async () => { + const cacheSetSpy = jest.spyOn(testCache, 'set'); manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', - headers: {} - }) - manager.start() - await manager.onReady() - await advanceTimersByTime(50) - expect(manager.get()).toEqual({ foo: 'bar' }) - expect(cacheSetSpy).toBeCalledWith('opt-datafile-keyThatExists', {"foo": "bar"}) - }) - }) + headers: {}, + }); + manager.start(); + await manager.onReady(); + await advanceTimersByTime(50); + expect(manager.get()).toEqual({ foo: 'bar' }); + expect(cacheSetSpy).toBeCalledWith('opt-datafile-keyThatExists', { foo: 'bar' }); + }); + }); describe('when constructed with a cache implementation without an already cached datafile', () => { beforeEach(() => { - manager = new TestDatafileManager({ + manager = new TestDatafileManager({ sdkKey: 'keyThatDoesExists', updateInterval: 500, autoUpdate: true, cache: testCache, - }) - manager.simulateResponseDelay = true - }) + }); + manager.simulateResponseDelay = true; + }); - it('does not find cached datafile, fetches new datafile from network, resolves promise and does not trigger update event', async() => { + it('does not find cached datafile, fetches new datafile from network, resolves promise and does not trigger update event', async () => { manager.queuedResponses.push({ statusCode: 200, body: '{"foo": "bar"}', - headers: {} - }) - - const updateFn = jest.fn() - manager.on('update', updateFn) - manager.start() - await advanceTimersByTime(50) - await manager.onReady() - expect(manager.get()).toEqual({ foo: 'bar' }) - expect(updateFn).toBeCalledTimes(0) - }) - }) -}) + headers: {}, + }); + + const updateFn = jest.fn(); + manager.on('update', updateFn); + manager.start(); + await advanceTimersByTime(50); + await manager.onReady(); + expect(manager.get()).toEqual({ foo: 'bar' }); + expect(updateFn).toBeCalledTimes(0); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/nodeDatafileManager.spec.ts b/packages/datafile-manager/__test__/nodeDatafileManager.spec.ts index 1d90d6677..44dec20dc 100644 --- a/packages/datafile-manager/__test__/nodeDatafileManager.spec.ts +++ b/packages/datafile-manager/__test__/nodeDatafileManager.spec.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import NodeDatafileManager from '../src/nodeDatafileManager' -import * as nodeRequest from '../src/nodeRequest' -import { Headers, AbortableRequest } from '../src/http' +import NodeDatafileManager from '../src/nodeDatafileManager'; +import * as nodeRequest from '../src/nodeRequest'; +import { Headers, AbortableRequest } from '../src/http'; import { advanceTimersByTime, getTimerCount } from './testUtils'; describe('nodeDatafileManager', () => { - let makeGetRequestSpy: jest.SpyInstance + let makeGetRequestSpy: jest.SpyInstance; beforeEach(() => { - jest.useFakeTimers() - makeGetRequestSpy = jest.spyOn(nodeRequest, 'makeGetRequest') - }) + jest.useFakeTimers(); + makeGetRequestSpy = jest.spyOn(nodeRequest, 'makeGetRequest'); + }); afterEach(() => { - jest.restoreAllMocks() - jest.clearAllTimers() - }) + jest.restoreAllMocks(); + jest.clearAllTimers(); + }); it('calls nodeEnvironment.makeGetRequest when started', async () => { makeGetRequestSpy.mockReturnValue({ @@ -38,21 +38,21 @@ describe('nodeDatafileManager', () => { statusCode: 200, body: '{"foo":"bar"}', headers: {}, - }) - }) + }), + }); const manager = new NodeDatafileManager({ sdkKey: '1234', autoUpdate: false, - }) - manager.start() - expect(makeGetRequestSpy).toBeCalledTimes(1) - expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json') - expect(makeGetRequestSpy.mock.calls[0][1]).toEqual({}) + }); + manager.start(); + expect(makeGetRequestSpy).toBeCalledTimes(1); + expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json'); + expect(makeGetRequestSpy.mock.calls[0][1]).toEqual({}); - await manager.onReady() - await manager.stop() - }) + await manager.onReady(); + await manager.stop(); + }); it('calls nodeEnvironment.makeGetRequest for live update requests', async () => { makeGetRequestSpy.mockReturnValue({ @@ -63,23 +63,23 @@ describe('nodeDatafileManager', () => { headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, - }) - }) + }), + }); const manager = new NodeDatafileManager({ sdkKey: '1234', autoUpdate: true, - }) - manager.start() - await manager.onReady() - await advanceTimersByTime(300000) - expect(makeGetRequestSpy).toBeCalledTimes(2) - expect(makeGetRequestSpy.mock.calls[1][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json') + }); + manager.start(); + await manager.onReady(); + await advanceTimersByTime(300000); + expect(makeGetRequestSpy).toBeCalledTimes(2); + expect(makeGetRequestSpy.mock.calls[1][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json'); expect(makeGetRequestSpy.mock.calls[1][1]).toEqual({ - 'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT' - }) + 'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT', + }); - await manager.stop() - }) + await manager.stop(); + }); it('defaults to true for autoUpdate', async () => { makeGetRequestSpy.mockReturnValue({ @@ -90,18 +90,18 @@ describe('nodeDatafileManager', () => { headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT', }, - }) - }) + }), + }); const manager = new NodeDatafileManager({ sdkKey: '1234', - }) - manager.start() - await manager.onReady() + }); + manager.start(); + await manager.onReady(); // Should set a timeout for a later update - expect(getTimerCount()).toBe(1) - await advanceTimersByTime(300000) - expect(makeGetRequestSpy).toBeCalledTimes(2) + expect(getTimerCount()).toBe(1); + await advanceTimersByTime(300000); + expect(makeGetRequestSpy).toBeCalledTimes(2); - await manager.stop() - }) -}) + await manager.stop(); + }); +}); diff --git a/packages/datafile-manager/__test__/nodeRequest.spec.ts b/packages/datafile-manager/__test__/nodeRequest.spec.ts index c9154e5fe..b8e6220b9 100644 --- a/packages/datafile-manager/__test__/nodeRequest.spec.ts +++ b/packages/datafile-manager/__test__/nodeRequest.spec.ts @@ -14,80 +14,84 @@ * limitations under the License. */ -import nock from 'nock' -import { makeGetRequest } from '../src/nodeRequest' -import { advanceTimersByTime } from './testUtils' +import nock from 'nock'; +import { makeGetRequest } from '../src/nodeRequest'; +import { advanceTimersByTime } from './testUtils'; beforeAll(() => { - nock.disableNetConnect() -}) + nock.disableNetConnect(); +}); afterAll(() => { - nock.enableNetConnect() -}) + nock.enableNetConnect(); +}); describe('nodeEnvironment', () => { - const host = 'https://cdn.optimizely.com' - const path = '/datafiles/123.json' + const host = 'https://cdn.optimizely.com'; + const path = '/datafiles/123.json'; afterEach(async () => { - nock.cleanAll() - }) + nock.cleanAll(); + }); describe('makeGetRequest', () => { it('returns a 200 response back to its superclass', async () => { const scope = nock(host) .get(path) - .reply(200, '{"foo":"bar"}') - const req = makeGetRequest(`${host}${path}`, {}) - const resp = await req.responsePromise + .reply(200, '{"foo":"bar"}'); + const req = makeGetRequest(`${host}${path}`, {}); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, body: '{"foo":"bar"}', headers: {}, - }) - scope.done() - }) + }); + scope.done(); + }); it('returns a 404 response back to its superclass', async () => { const scope = nock(host) .get(path) - .reply(404, '') - const req = makeGetRequest(`${host}${path}`, {}) - const resp = await req.responsePromise + .reply(404, ''); + const req = makeGetRequest(`${host}${path}`, {}); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 404, body: '', headers: {}, - }) - scope.done() - }) + }); + scope.done(); + }); it('includes headers from the headers argument in the request', async () => { const scope = nock(host) .matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT') .get(path) - .reply(304, '') + .reply(304, ''); const req = makeGetRequest(`${host}${path}`, { 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', - }) - const resp = await req.responsePromise + }); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 304, body: '', headers: {}, - }) - scope.done() - }) + }); + scope.done(); + }); it('includes headers from the response in the eventual response in the return value', async () => { const scope = nock(host) .get(path) - .reply(200, { foo: 'bar' }, { - 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', - }) - const req = makeGetRequest(`${host}${path}`, {}) - const resp = await req.responsePromise + .reply( + 200, + { foo: 'bar' }, + { + 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', + } + ); + const req = makeGetRequest(`${host}${path}`, {}); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, body: '{"foo":"bar"}', @@ -95,40 +99,40 @@ describe('nodeEnvironment', () => { 'content-type': 'application/json', 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', }, - }) - scope.done() - }) + }); + scope.done(); + }); it('handles a URL with a query string', async () => { - const pathWithQuery = '/datafiles/123.json?from_my_app=true' + const pathWithQuery = '/datafiles/123.json?from_my_app=true'; const scope = nock(host) .get(pathWithQuery) - .reply(200, { foo: 'bar' }) - const req = makeGetRequest(`${host}${pathWithQuery}`, {}) - await req.responsePromise - scope.done() - }) + .reply(200, { foo: 'bar' }); + const req = makeGetRequest(`${host}${pathWithQuery}`, {}); + await req.responsePromise; + scope.done(); + }); it('handles a URL with http protocol (not https)', async () => { - const httpHost = 'http://cdn.optimizely.com' + const httpHost = 'http://cdn.optimizely.com'; const scope = nock(httpHost) .get(path) - .reply(200, '{"foo":"bar"}') - const req = makeGetRequest(`${httpHost}${path}`, {}) - const resp = await req.responsePromise + .reply(200, '{"foo":"bar"}'); + const req = makeGetRequest(`${httpHost}${path}`, {}); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, body: '{"foo":"bar"}', - headers: {} - }) - scope.done() - }) + headers: {}, + }); + scope.done(); + }); it('returns a rejected response promise when the URL protocol is unsupported', async () => { - const invalidProtocolUrl = 'ftp://something/datafiles/123.json' - const req = makeGetRequest(invalidProtocolUrl, {}) - await expect(req.responsePromise).rejects.toThrow() - }) + const invalidProtocolUrl = 'ftp://something/datafiles/123.json'; + const req = makeGetRequest(invalidProtocolUrl, {}); + await expect(req.responsePromise).rejects.toThrow(); + }); it('returns a rejected promise when there is a request error', async () => { const scope = nock(host) @@ -136,62 +140,62 @@ describe('nodeEnvironment', () => { .replyWithError({ message: 'Connection error', code: 'CONNECTION_ERROR', - }) - const req = makeGetRequest(`${host}${path}`, {}) - await expect(req.responsePromise).rejects.toThrow() - scope.done() - }) + }); + const req = makeGetRequest(`${host}${path}`, {}); + await expect(req.responsePromise).rejects.toThrow(); + scope.done(); + }); it('handles a url with a host and a port', async () => { - const hostWithPort = 'http://datafiles:3000' - const path = '/12/345.json' + const hostWithPort = 'http://datafiles:3000'; + const path = '/12/345.json'; const scope = nock(hostWithPort) .get(path) - .reply(200, '{"foo":"bar"}') - const req = makeGetRequest(`${hostWithPort}${path}`, {}) - const resp = await req.responsePromise + .reply(200, '{"foo":"bar"}'); + const req = makeGetRequest(`${hostWithPort}${path}`, {}); + const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, body: '{"foo":"bar"}', - headers: {} - }) - scope.done() - }) + headers: {}, + }); + scope.done(); + }); describe('timeout', () => { beforeEach(() => { - jest.useFakeTimers() - }) + jest.useFakeTimers(); + }); afterEach(() => { - jest.clearAllTimers() - }) + jest.clearAllTimers(); + }); it('rejects the response promise and aborts the request when the response is not received before the timeout', async () => { const scope = nock(host) .get(path) .delay(61000) - .reply(200, '{"foo":"bar"}') - - const abortEventListener = jest.fn() - let emittedReq: any - const requestListener = (request: any) => { - emittedReq = request - emittedReq.once('abort', abortEventListener) - } - scope.on('request', requestListener) - - const req = makeGetRequest(`${host}${path}`, {}) - await advanceTimersByTime(60000) - await expect(req.responsePromise).rejects.toThrow() - expect(abortEventListener).toBeCalledTimes(1) - - scope.done() + .reply(200, '{"foo":"bar"}'); + + const abortEventListener = jest.fn(); + let emittedReq: any; + const requestListener = (request: any): void => { + emittedReq = request; + emittedReq.once('abort', abortEventListener); + }; + scope.on('request', requestListener); + + const req = makeGetRequest(`${host}${path}`, {}); + await advanceTimersByTime(60000); + await expect(req.responsePromise).rejects.toThrow(); + expect(abortEventListener).toBeCalledTimes(1); + + scope.done(); if (emittedReq) { - emittedReq.off('abort', abortEventListener) + emittedReq.off('abort', abortEventListener); } - scope.off('request', requestListener) - }) - }) - }) -}) + scope.off('request', requestListener); + }); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/reactNativeAsyncStorageCache.spec.ts b/packages/datafile-manager/__test__/reactNativeAsyncStorageCache.spec.ts index 02f4eff0b..9084434de 100644 --- a/packages/datafile-manager/__test__/reactNativeAsyncStorageCache.spec.ts +++ b/packages/datafile-manager/__test__/reactNativeAsyncStorageCache.spec.ts @@ -14,52 +14,57 @@ * limitations under the License. */ -import ReactNativeAsyncStorageCache from '../src/reactNativeAsyncStorageCache' +import ReactNativeAsyncStorageCache from '../src/reactNativeAsyncStorageCache'; describe('reactNativeAsyncStorageCache', () => { - let cacheInstance: ReactNativeAsyncStorageCache + let cacheInstance: ReactNativeAsyncStorageCache; beforeEach(() => { - cacheInstance = new ReactNativeAsyncStorageCache() - }) + cacheInstance = new ReactNativeAsyncStorageCache(); + }); describe('get', function() { it('should return correct object when item is found in cache', function() { - return cacheInstance.get('keyThatExists') - .then(v => expect(v).toEqual({ name: "Awesome Object" })) - }) + return cacheInstance.get('keyThatExists').then(v => expect(v).toEqual({ name: 'Awesome Object' })); + }); it('should return null if item is not found in cache', function() { - return cacheInstance.get('keyThatDoesNotExist').then(v => expect(v).toBeNull()) - }) + return cacheInstance.get('keyThatDoesNotExist').then(v => expect(v).toBeNull()); + }); it('should reject promise error if string has an incorrect JSON format', function() { - return cacheInstance.get('keyWithInvalidJsonObject') - .catch(() => 'exception caught').then(v => { expect(v).toEqual('exception caught') }) - }) - }) + return cacheInstance + .get('keyWithInvalidJsonObject') + .catch(() => 'exception caught') + .then(v => { + expect(v).toEqual('exception caught'); + }); + }); + }); - describe('set', function() { + describe('set', function() { it('should resolve promise if item was successfully set in the cache', function() { - const testObj = { name: "Awesome Object" } - return cacheInstance.set('testKey', testObj) - }) + const testObj = { name: 'Awesome Object' }; + return cacheInstance.set('testKey', testObj); + }); it('should reject promise if item was not set in the cache because of json stringifying error', function() { - const testObj: any = { name: "Awesome Object" } - testObj.myOwnReference = testObj - return cacheInstance.set('testKey', testObj) - .catch(() => 'exception caught').then(v => expect(v).toEqual('exception caught')) - }) - }) + const testObj: any = { name: 'Awesome Object' }; + testObj.myOwnReference = testObj; + return cacheInstance + .set('testKey', testObj) + .catch(() => 'exception caught') + .then(v => expect(v).toEqual('exception caught')); + }); + }); describe('contains', function() { it('should return true if object with key exists', function() { - return cacheInstance.contains('keyThatExists').then(v => expect(v).toBeTruthy()) - }) + return cacheInstance.contains('keyThatExists').then(v => expect(v).toBeTruthy()); + }); it('should return false if object with key does not exist', function() { - return cacheInstance.contains('keyThatDoesNotExist').then(v => expect(v).toBeFalsy()) - }) - }) -}) + return cacheInstance.contains('keyThatDoesNotExist').then(v => expect(v).toBeFalsy()); + }); + }); +}); diff --git a/packages/datafile-manager/__test__/testUtils.ts b/packages/datafile-manager/__test__/testUtils.ts index 9d58c593b..ac590b978 100644 --- a/packages/datafile-manager/__test__/testUtils.ts +++ b/packages/datafile-manager/__test__/testUtils.ts @@ -15,13 +15,13 @@ */ export function advanceTimersByTime(waitMs: number): Promise { - const timeoutPromise: Promise = new Promise(res => setTimeout(res, waitMs)) - jest.advanceTimersByTime(waitMs) - return timeoutPromise + const timeoutPromise: Promise = new Promise(res => setTimeout(res, waitMs)); + jest.advanceTimersByTime(waitMs); + return timeoutPromise; } export function getTimerCount(): number { // Type definition for jest doesn't include this, but it exists // https://jestjs.io/docs/en/jest-object#jestgettimercount - return (jest as any).getTimerCount() + return (jest as any).getTimerCount(); } diff --git a/packages/datafile-manager/package.json b/packages/datafile-manager/package.json index 9f807cd5a..7235a30ff 100644 --- a/packages/datafile-manager/package.json +++ b/packages/datafile-manager/package.json @@ -49,7 +49,7 @@ "@react-native-community/async-storage": "^1.2.0" }, "scripts": { - "lint": "tsc --noEmit && eslint --fix 'src/**/*.ts'", + "lint": "tsc --noEmit && eslint --fix 'src/**/*.ts' '__test__/**/*.ts'", "test": "jest", "tsc": "rm -rf lib && tsc", "prepublishOnly": "npm run lint && npm test && npm run tsc"