UNPKG

box-ui-elements-mlh

Version:
439 lines (383 loc) 17.1 kB
import noop from 'lodash/noop'; import TokenService from '../TokenService'; import Xhr from '../Xhr'; jest.mock('../TokenService'); TokenService.getReadToken.mockImplementation(() => Promise.resolve(`${Math.random()}`)); describe('util/Xhr', () => { let xhrInstance; beforeEach(() => { xhrInstance = new Xhr({ token: '123', }); }); describe('get()', () => { test('should make get call with axios', () => { const url = 'parsedurl'; xhrInstance.getParsedUrl = jest.fn().mockReturnValue(url); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); xhrInstance.axios = { get: jest.fn().mockReturnValue({}), }; return xhrInstance .get({ url: 'url', data: {}, }) .then(() => { expect(xhrInstance.axios.get).toHaveBeenCalledWith('url', { cancelToken: xhrInstance.axiosSource.token, params: {}, headers: {}, parsedUrl: url, }); }); }); }); describe('post()', () => { test('should make post call with axios', () => { const url = 'parsedurl'; xhrInstance.getParsedUrl = jest.fn().mockReturnValue(url); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); xhrInstance.axios = jest.fn().mockReturnValue({}); return xhrInstance .post({ url: 'url', data: {}, }) .then(() => { expect(xhrInstance.axios).toHaveBeenCalledWith({ url: 'url', method: 'POST', parsedUrl: url, data: {}, headers: {}, }); }); }); }); describe('put()', () => { test('should call post() with put method', () => { xhrInstance.post = jest.fn(); xhrInstance.put({ id: '123', url: 'url', data: {}, }); expect(xhrInstance.post).toHaveBeenCalledWith({ id: '123', url: 'url', data: {}, method: 'PUT', headers: {}, }); }); }); describe('delete()', () => { test('should call post() with delete method', () => { xhrInstance.post = jest.fn(); xhrInstance.delete({ id: '123', url: 'url', data: {}, }); expect(xhrInstance.post).toHaveBeenCalledWith({ id: '123', url: 'url', data: {}, method: 'DELETE', headers: {}, }); }); }); describe('options()', () => { test('should make options call with axios and call successHandler on success', () => { const response = { data: {} }; const successHandler = jest.fn(); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); xhrInstance.axios = jest.fn().mockReturnValue(Promise.resolve(response)); return xhrInstance .options({ successHandler, errorHandler: noop, }) .then(() => { expect(xhrInstance.axios).toHaveBeenCalledWith({ method: 'OPTIONS', headers: {}, }); expect(successHandler).toHaveBeenCalledWith(response); }); }); test('should call errorHandler on axios error', () => { const error = { status: '' }; const errorHandler = jest.fn(); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); xhrInstance.axios = jest.fn().mockReturnValue(Promise.reject(error)); return xhrInstance .options({ successHandler: noop, errorHandler, }) .then(() => { expect(xhrInstance.axios).toHaveBeenCalledWith({ method: 'OPTIONS', headers: {}, }); expect(errorHandler).toHaveBeenCalledWith(error); }); }); test('should call errorHandler on getHeaders error', () => { const error = { status: '' }; const errorHandler = jest.fn(); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.reject(error)); return xhrInstance .options({ successHandler: noop, errorHandler, }) .then(() => { expect(errorHandler).toHaveBeenCalledWith(error); }); }); }); describe('uploadFile()', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.clearAllTimers(); }); test('should call abort & idleTimeoutHandler if there is no upload progress after idleTimeoutDuration', () => { xhrInstance.abort = jest.fn(); xhrInstance.axios = jest.fn(); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); const idleTimoutHandler = jest.fn(); return xhrInstance .uploadFile({ successHandler: noop, errorHandler: noop, progressHandler: noop, withIdleTimeout: true, idleTimeoutDuration: 100, idleTimeoutHandler: idleTimoutHandler, }) .then(() => { jest.advanceTimersByTime(101); // 101ms should trigger idle timeout func that calls abort expect(xhrInstance.abort).toHaveBeenCalled(); expect(idleTimoutHandler).toHaveBeenCalled(); }); }); test('should not call abort if there is upload progress before idleTimeoutDuration', () => { const uploadHandler = jest.fn(); xhrInstance.abort = jest.fn(); xhrInstance.axios = jest.fn(({ onUploadProgress }) => { jest.advanceTimersByTime(50); // simulate progress event after 50ms onUploadProgress(); }); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); return xhrInstance .uploadFile({ successHandler: noop, errorHandler: noop, progressHandler: uploadHandler, withIdleTimeout: true, idleTimeoutDuration: 100, }) .then(() => { jest.advanceTimersByTime(51); // 50 + 51ms will original idle timeout func unless cancelled expect(uploadHandler).toHaveBeenCalled(); expect(xhrInstance.abort).not.toHaveBeenCalled(); }); }); test('should call successHandler and not call abort if upload succeeds', () => { const successHandler = jest.fn(); const response = { data: {} }; xhrInstance.abort = jest.fn(); xhrInstance.axios = jest.fn().mockReturnValue(Promise.resolve(response)); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); return xhrInstance .uploadFile({ successHandler, errorHandler: noop, progressHandler: noop, withIdleTimeout: true, idleTimeoutDuration: 100, }) .then(() => { jest.advanceTimersByTime(101); expect(xhrInstance.abort).not.toHaveBeenCalled(); expect(successHandler).toHaveBeenCalledWith(response); }); }); test('should call errorHandler and not call abort if upload fails', () => { const errorHandler = jest.fn(); const error = { status: '' }; xhrInstance.abort = jest.fn(); xhrInstance.axios = jest.fn().mockReturnValue(Promise.reject(error)); xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.resolve({})); return xhrInstance .uploadFile({ successHandler: noop, errorHandler, progressHandler: noop, withIdleTimeout: true, idleTimeoutDuration: 100, }) .then(() => { jest.advanceTimersByTime(101); expect(xhrInstance.abort).not.toHaveBeenCalled(); expect(errorHandler).toHaveBeenCalledWith(error); }); }); test('should call errorHandler if getHeaders fails', () => { const errorHandler = jest.fn(); const error = { status: '' }; xhrInstance.getHeaders = jest.fn().mockReturnValue(Promise.reject(error)); return xhrInstance .uploadFile({ successHandler: noop, errorHandler, progressHandler: noop, }) .then(() => { expect(errorHandler).toHaveBeenCalledWith(error); }); }); }); describe('abort()', () => { test('should cancel axios request', () => { const mockSource = { cancel: jest.fn(), }; xhrInstance.axiosSource = mockSource; xhrInstance.abort(); expect(mockSource.cancel).toHaveBeenCalled(); }); }); describe('defaultResponseInterceptor()', () => { test('should return the response', () => { const origResponse = { status: 500, foo: 'bar', }; const response = xhrInstance.defaultResponseInterceptor(origResponse); expect(response).toEqual(origResponse); }); }); describe('shouldRetryRequest', () => { const createXhrInstance = options => { xhrInstance = new Xhr({ token: '123', ...options, }); }; test.each` condition | shouldRetry | method | retryableStatusCodes | responseCode | hasRequestBody | retryCount | expected ${'shouldRetry=false'} | ${false} | ${'get'} | ${undefined} | ${429} | ${true} | ${0} | ${false} ${'max retries hit'} | ${true} | ${'get'} | ${undefined} | ${429} | ${true} | ${3} | ${false} ${'invalid status 5xx'} | ${true} | ${'get'} | ${undefined} | ${500} | ${true} | ${0} | ${false} ${'invalid status 4xx'} | ${true} | ${'get'} | ${undefined} | ${404} | ${true} | ${0} | ${false} ${'error was thrown'} | ${true} | ${'get'} | ${undefined} | ${undefined} | ${false} | ${0} | ${false} ${'unsafe http method POST'} | ${true} | ${'post'} | ${[500]} | ${500} | ${true} | ${0} | ${false} ${'unsafe http method PUT'} | ${true} | ${'put'} | ${[500]} | ${500} | ${true} | ${0} | ${false} ${'unsafe http method DELETE'} | ${true} | ${'delete'} | ${[500]} | ${500} | ${true} | ${0} | ${false} ${'unsafe method w/429 code'} | ${true} | ${'post'} | ${undefined} | ${429} | ${true} | ${0} | ${true} ${'rate limit status 429'} | ${true} | ${'get'} | ${undefined} | ${429} | ${true} | ${0} | ${true} ${'custom retryable statuses'} | ${true} | ${'get'} | ${[503, 429]} | ${503} | ${true} | ${0} | ${true} ${'generic error is thrown'} | ${true} | ${'get'} | ${undefined} | ${undefined} | ${true} | ${0} | ${true} `( `should retry = $expected when $condition`, ({ shouldRetry, method, retryableStatusCodes, responseCode, hasRequestBody, retryCount, expected }) => { createXhrInstance({ shouldRetry, retryableStatusCodes }); xhrInstance.retryCount = retryCount; const result = xhrInstance.shouldRetryRequest({ response: responseCode ? { status: responseCode } : undefined, request: hasRequestBody ? { data: { foo: 'bar' } } : undefined, config: { method }, // AxiosXHRConfig for the request }); expect(result).toBe(expected); }, ); }); describe('getExponentialRetryTimeoutInMs()', () => { beforeEach(() => { jest.spyOn(Math, 'random').mockReturnValue(0.5); }); test.each([ [1, 1500], [2, 2500], [3, 4500], [4, 8500], ])('should get exponential retry timeout %#', (retryCount, expected) => { expect(xhrInstance.getExponentialRetryTimeoutInMs(retryCount)).toBe(expected); }); }); describe('errorInterceptor()', () => { const DELAY = 500; beforeEach(() => { xhrInstance.shouldRetryRequest = jest.fn(); xhrInstance.getExponentialRetryTimeoutInMs = jest.fn().mockReturnValue(DELAY); xhrInstance.responseInterceptor = jest.fn(); jest.useFakeTimers(); }); test('should retry the request before calling the error interceptor', () => { const error = { status: 429, response: { data: undefined, }, }; xhrInstance.axios = jest.fn().mockImplementation(() => { xhrInstance .errorInterceptor(error) .then(() => {}) .catch(() => {}); return Promise.resolve(); }); // first time return true, then false xhrInstance.shouldRetryRequest.mockReturnValue(false).mockReturnValueOnce(true); xhrInstance.errorInterceptor(error); expect(xhrInstance.retryCount).toBe(1); expect(xhrInstance.getExponentialRetryTimeoutInMs).toHaveBeenCalled(); expect(xhrInstance.responseInterceptor).not.toHaveBeenCalled(); jest.runAllTimers(); expect(xhrInstance.axios).toHaveBeenCalled(); expect(xhrInstance.retryCount).toBe(1); expect(xhrInstance.responseInterceptor).toHaveBeenCalledWith(error); }); test('should not retry the request before calling the error interceptor', () => { expect.assertions(3); const response = { data: { foo: 'bar', }, }; const error = { status: 500, response, }; xhrInstance.axios = jest.fn().mockImplementation(() => { xhrInstance.errorInterceptor(error); return Promise.resolve(); }); xhrInstance.shouldRetryRequest.mockReturnValue(false); xhrInstance.errorInterceptor(error).catch(() => { expect(xhrInstance.getExponentialRetryTimeoutInMs).not.toHaveBeenCalled(); expect(xhrInstance.axios).not.toHaveBeenCalled(); expect(xhrInstance.responseInterceptor).toHaveBeenCalledWith(response.data); }); }); }); describe('getHeaders()', () => { it('should not override any existing Accept-Language header', async () => { xhrInstance.language = 'bar'; const actHeaders = await xhrInstance.getHeaders('123', { 'Accept-Language': 'foo' }); expect(actHeaders['Accept-Language']).toBe('foo'); }); it('should apply Accept-Language header if language exists', async () => { xhrInstance.language = 'bar'; const actHeaders = await xhrInstance.getHeaders('123'); expect(actHeaders['Accept-Language']).toBe('bar'); }); }); });