diff --git a/src/lib/Preview.js b/src/lib/Preview.js index b8c46ee1c6..3753b7d061 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -265,6 +265,9 @@ class Preview extends EventEmitter { // Clean the UI this.ui.cleanup(); + // Eject http interceptors + api.ejectInterceptors(); + // Nuke the file this.file = undefined; } @@ -773,6 +776,15 @@ class Preview extends EventEmitter { this.retryCount = 0; } + // Check to see if there are http interceptors and load them + if (this.options.responseInterceptor) { + api.addResponseInterceptor(this.options.responseInterceptor); + } + + if (this.options.requestInterceptor) { + api.addRequestInterceptor(this.options.requestInterceptor); + } + // @TODO: This may not be the best way to detect if we are offline. Make sure this works well if we decided to // combine Box Elements + Preview. This could potentially break if we have Box Elements fetch the file object // and pass the well-formed file object directly to the preview library to render. @@ -803,6 +815,15 @@ class Preview extends EventEmitter { this.setupUI(); } + // Check to see if there are http interceptors and load them + if (this.options.responseInterceptor) { + api.addResponseInterceptor(this.options.responseInterceptor); + } + + if (this.options.requestInterceptor) { + api.addRequestInterceptor(this.options.requestInterceptor); + } + // Load from cache if the current file is valid, otherwise load file info from server if (checkFileValid(this.file)) { // Save file in cache. This also adds the 'ORIGINAL' representation. It is required to preview files offline @@ -942,6 +963,12 @@ class Preview extends EventEmitter { // Prefix any user created loaders before our default ones this.loaders = (options.loaders || []).concat(loaderList); + // Add the request interceptor to the preview instance + this.options.requestInterceptor = options.requestInterceptor; + + // Add the response interceptor to the preview instance + this.options.responseInterceptor = options.responseInterceptor; + // Disable or enable viewers based on viewer options Object.keys(this.options.viewers).forEach(viewerName => { const isDisabled = this.options.viewers[viewerName].disabled; diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 21a42f9551..07720772b2 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -950,6 +950,9 @@ describe('lib/Preview', () => { stubs.getTokens = sandbox.stub(tokens, 'default').returns(stubs.promise); stubs.handleTokenResponse = sandbox.stub(preview, 'handleTokenResponse'); + stubs.apiAddRequestInterceptor = sandbox.stub(api, 'addRequestInterceptor'); + stubs.apiAddResponseInterceptor = sandbox.stub(api, 'addResponseInterceptor'); + stubs.get = sandbox.stub(preview.cache, 'get'); stubs.destroy = sandbox.stub(preview, 'destroy'); @@ -1091,16 +1094,30 @@ describe('lib/Preview', () => { expect(stubs.handleTokenResponse).to.be.called; }); }); + + it('should load response interceptor if an option', () => { + preview.options.responseInterceptor = sandbox.stub(); + + preview.load('0'); + expect(stubs.apiAddResponseInterceptor).to.be.called; + }); + + it('should load request interceptor if an option', () => { + preview.options.requestInterceptor = sandbox.stub(); + + preview.load('0'); + expect(stubs.apiAddRequestInterceptor).to.be.called; + }); }); describe('handleTokenResponse()', () => { beforeEach(() => { - stubs.loadFromServer = sandbox.stub(preview, 'loadFromServer'); - stubs.setupUI = sandbox.stub(preview, 'setupUI'); - stubs.checkPermission = sandbox.stub(file, 'checkPermission'); + stubs.cacheFile = sandbox.stub(file, 'cacheFile'); stubs.checkFileValid = sandbox.stub(file, 'checkFileValid'); + stubs.checkPermission = sandbox.stub(file, 'checkPermission'); stubs.loadFromCache = sandbox.stub(preview, 'loadFromCache'); - stubs.cacheFile = sandbox.stub(file, 'cacheFile'); + stubs.loadFromServer = sandbox.stub(preview, 'loadFromServer'); + stubs.setupUI = sandbox.stub(preview, 'setupUI'); stubs.ui = sandbox.stub(preview.ui, 'isSetup'); }); @@ -1315,6 +1332,22 @@ describe('lib/Preview', () => { preview.parseOptions(preview.previewOptions); expect(preview.options.enableThumbnailsSidebar).to.be.true; }); + + it('should set the request interceptor if provided', () => { + const requestInterceptor = sandbox.stub(); + preview.previewOptions.requestInterceptor = requestInterceptor; + preview.parseOptions(preview.previewOptions); + + expect(preview.options.requestInterceptor).to.equal(requestInterceptor); + }); + + it('should set the response interceptor if provided', () => { + const responseInterceptor = sandbox.stub(); + preview.previewOptions.responseInterceptor = responseInterceptor; + preview.parseOptions(preview.previewOptions); + + expect(preview.options.responseInterceptor).to.equal(responseInterceptor); + }); }); describe('createViewerOptions()', () => { diff --git a/src/lib/__tests__/api-test.js b/src/lib/__tests__/api-test.js index 32ce26ef7d..69b022fd19 100644 --- a/src/lib/__tests__/api-test.js +++ b/src/lib/__tests__/api-test.js @@ -8,16 +8,19 @@ describe('API helper', () => { }); describe('parseResponse()', () => { + const url = '/foo/bar'; + it('should return the full response when the status is 202 or 204', () => { const response = { status: 202, data: 'foo', }; - expect(api.parseResponse(response)).to.equal(response); + sandbox.stub(api, 'client').resolves(response); - response.status = 204; - expect(api.parseResponse(response)).to.equal(response); + return api.get(url).then(data => { + expect(data).to.equal(response); + }); }); it('should only return the data', () => { @@ -27,7 +30,11 @@ describe('API helper', () => { data, }; - expect(api.parseResponse(response)).to.equal(data); + sandbox.stub(api, 'client').resolves(response); + + return api.get(url).then(returnedData => { + expect(returnedData).to.equal(data); + }); }); }); @@ -182,4 +189,41 @@ describe('API helper', () => { }); }); }); + + describe('addResponseInterceptor', () => { + it('should add an http response interceptor', () => { + const responseInterceptor = sinon.stub(); + api.addResponseInterceptor(responseInterceptor); + + expect(api.client.interceptors.response.handlers[0].fulfilled).to.equal(responseInterceptor); + api.ejectInterceptors(); + }); + }); + + describe('addRequestInterceptor', () => { + it('should add an http request interceptor', () => { + const requestInterceptor = sinon.stub(); + api.addRequestInterceptor(requestInterceptor); + + expect(api.client.interceptors.request.handlers[0].fulfilled).to.equal(requestInterceptor); + api.ejectInterceptors(); + }); + }); + + describe('ejectInterceptors', () => { + it('should remove all interceptors', () => { + const requestInterceptor = sinon.stub(); + const responseInterceptor = sinon.stub(); + + api.addRequestInterceptor(requestInterceptor); + api.addRequestInterceptor(requestInterceptor); + api.addResponseInterceptor(responseInterceptor); + api.addResponseInterceptor(responseInterceptor); + + api.ejectInterceptors(); + + expect(api.client.interceptors.request.handlers[0]).to.equal(null); + expect(api.client.interceptors.response.handlers[0]).to.equal(null); + }); + }); }); diff --git a/src/lib/api.js b/src/lib/api.js index 9e66c2748a..0695f076cf 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,77 +1,111 @@ import axios from 'axios'; -const api = { - /** - * Filter empty values from the Axios request options object - * - * @private - * @param {Object} options - The request options - * @return {Object} The cleaned request options - */ - filterOptions(options = {}) { - const result = {}; +/** + * Retrieves JSON from response. + * + * @private + * @param {Response} response - Response to parse + * @return {Promise|Response} Response if 204 or 202, otherwise promise that resolves with JSON + */ +const parseResponse = response => { + if (response.status === 204 || response.status === 202) { + return response; + } + + return response.data; +}; - Object.keys(options).forEach(key => { - if (options[key] !== undefined && options[key] !== null && options[key] !== '') { - result[key] = options[key]; - } - }); +/** + * Filter empty values from the http request options object + * + * @private + * @param {Object} options - The request options + * @return {Object} The cleaned request options + */ +const filterOptions = (options = {}) => { + const result = {}; + + Object.keys(options).forEach(key => { + if (options[key] !== undefined && options[key] !== null && options[key] !== '') { + result[key] = options[key]; + } + }); + + return result; +}; - return result; - }, +/** + * Helper function to convert an http error to the format Preview expects + * + * @private + * @param {Object} response - Axios error response + * @throws {Error} - Throws when an error response object exists + * @return {void} + */ +const handleError = ({ response }) => { + if (response) { + const error = new Error(response.statusText); + error.response = response; // Need to pass response through so we can see what kind of HTTP error this was + throw error; + } +}; +/** + * Pass through transformer if the response type is text + * @param {Object} data + * @return {Object} + */ +const transformTextResponse = data => data; + +class Api { /** - * Helper function to convert an Axios error to the format Preview expects + * [constructor] * - * @private - * @param {Object} response - Axios error response - * @throws {Error} - Throws when an error response object exists - * @return {void} + * @return {Api} Instance of the API */ - handleError({ response }) { - if (response) { - const error = new Error(response.statusText); - error.response = response; // Need to pass response through so we can see what kind of HTTP error this was - throw error; - } - }, + constructor() { + this.client = axios.create(); + } /** - * Retrieves JSON from response. - * - * @private - * @param {Response} response - Response to parse - * @return {Promise|Response} Response if 204 or 202, otherwise promise that resolves with JSON + * Adds a function that intercepts an http response + + * @public + * @param {Function} responseInterceptor - Function that gets called on each response + * @return {void} */ - parseResponse: response => { - if (response.status === 204 || response.status === 202) { - return response; + addResponseInterceptor(responseInterceptor) { + if (typeof responseInterceptor === 'function') { + this.client.interceptors.response.use(responseInterceptor); } + } - return response.data; - }, + /** + * Adds a function that intercepts an http request + * @public + * @param {Function} requestInterceptor - function that gets called on each request + * @return {void} - transformTextResponse: data => data, + */ + addRequestInterceptor(requestInterceptor) { + if (typeof requestInterceptor === 'function') { + this.client.interceptors.request.use(requestInterceptor); + } + } /** - * Wrapper function for XHR post put and delete + * Ejects all interceptors + * @public * - * @private - * @param {string} url - The URL for XHR - * @param {Object} options - The request options - * @return {Promise} - XHR promise + * @return {void} */ - xhr(url, options = {}) { - let transformResponse; - - if (options.responseType === 'text') { - transformResponse = api.transformTextResponse; - } - - return axios(url, api.filterOptions({ transformResponse, ...options })) - .then(api.parseResponse) - .catch(api.handleError); - }, + ejectInterceptors() { + ['response', 'request'].forEach(interceptorType => { + this.client.interceptors[interceptorType].handlers.forEach((interceptor, index) => { + this.client.interceptors[interceptorType].eject(index); + }); + }); + } /** * HTTP GETs a URL @@ -82,8 +116,8 @@ const api = { * @return {Promise} - HTTP response */ get(url, { type: responseType = 'json', ...options } = {}) { - return api.xhr(url, { method: 'get', responseType, ...options }); - }, + return this.xhr(url, { method: 'get', responseType, ...options }); + } /** * HTTP HEAD a URL @@ -94,8 +128,8 @@ const api = { * @return {Promise} HTTP response */ head(url, options = {}) { - return api.xhr(url, { method: 'head', ...options }); - }, + return this.xhr(url, { method: 'head', ...options }); + } /** * HTTP POSTs a URL with JSON data @@ -107,8 +141,8 @@ const api = { * @return {Promise} HTTP response */ post(url, data, options = {}) { - return api.xhr(url, { method: 'post', data, ...options }); - }, + return this.xhr(url, { method: 'post', data, ...options }); + } /** * HTTP DELETEs a URL with JSON data @@ -120,8 +154,8 @@ const api = { * @return {Promise} HTTP response */ delete(url, data, options = {}) { - return api.xhr(url, { method: 'delete', data, ...options }); - }, + return this.xhr(url, { method: 'delete', data, ...options }); + } /** * HTTP PUTs a url with JSON data @@ -133,8 +167,28 @@ const api = { * @return {Promise} HTTP response */ put(url, data, options = {}) { - return api.xhr(url, { method: 'put', data, ...options }); - }, -}; + return this.xhr(url, { method: 'put', data, ...options }); + } + + /** + * Wrapper function for XHR post put and delete + * + * @private + * @param {string} url - The URL for XHR + * @param {Object} options - The request options + * @return {Promise} - XHR promise + */ + xhr(url, options = {}) { + let transformResponse; + + if (options.responseType === 'text') { + transformResponse = transformTextResponse; + } + + return this.client(url, filterOptions({ transformResponse, ...options })) + .then(parseResponse) + .catch(handleError); + } +} -export default api; +export default new Api();