diff --git a/src/service.js b/src/service.js index a572e0e..6df7026 100644 --- a/src/service.js +++ b/src/service.js @@ -1,3 +1,5 @@ +import { reduceHandlers } from './utils'; + /** * @property {Array} middlewares stack * @property {AxiosInstance} http @@ -101,10 +103,22 @@ export default class HttpMiddlewareService { * @returns {Promise} */ adapter(config) { - return this.chain.reduce( - (acc, [onResolve, onError]) => acc.then(onResolve, onError), - Promise.resolve(config) - ); + const { request: requestHandlers, response } = this.chain; + return reduceHandlers(requestHandlers, Promise.resolve(config)) + .then( + (conf) => { + if (!conf) { + const err = Error('Request cancelled within a middleware'); + err.type = 'REQUEST_CANCELLED'; + return Promise.reject(err); + } + return reduceHandlers( + response, + this._onSync(this.originalAdapter.call(this.http, conf)) + ); + }, + err => reduceHandlers(response, Promise.reject(err)) + ); } /** @@ -113,12 +127,17 @@ export default class HttpMiddlewareService { * @private */ _addMiddleware(middleware) { - this.chain.unshift([ - middleware.onRequest && (conf => middleware.onRequest(conf)), + if (!this.chain) { + this._updateChain(); + return; + } + + this.chain.request.unshift([ + middleware.onRequest && (conf => conf && middleware.onRequest(conf)), middleware.onRequestError && (error => middleware.onRequestError(error)), ]); - this.chain.push([ + this.chain.response.push([ middleware.onResponse && (response => middleware.onResponse(response)), middleware.onResponseError && (error => middleware.onResponseError(error)), ]); @@ -128,7 +147,10 @@ export default class HttpMiddlewareService { * @private */ _updateChain() { - this.chain = [[conf => this._onSync(this.originalAdapter.call(this.http, conf)), undefined]]; + this.chain = { + request: [], + response: [], + }; this.middlewares.forEach(middleware => this._addMiddleware(middleware)); } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d163742 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,4 @@ +export const reduceHandlers = (handlers, promise) => handlers.reduce( + (acc, [onResolve, onError]) => acc.then(onResolve, onError), + promise +); diff --git a/test/mocks/MiddlewareMock.js b/test/mocks/MiddlewareMock.js index b4c6c6e..1a96c92 100644 --- a/test/mocks/MiddlewareMock.js +++ b/test/mocks/MiddlewareMock.js @@ -1,11 +1,14 @@ +const identity = a => a; +const reject = err => Promise.reject(err); + export default class MiddlewareMock { - constructor() { + constructor(id = 'MiddlewareMock') { Object.assign(this, { - onRequest: jest.fn(config => config), - onRequestError: jest.fn(), - onSync: jest.fn(promise => promise), - onResponse: jest.fn(response => response), - onResponseError: jest.fn(), + onRequest: jest.fn(identity).mockName(`${id}.onRequest`), + onRequestError: jest.fn(reject).mockName(`${id}.onRequestError`), + onSync: jest.fn(identity).mockName(`${id}.onSync`), + onResponse: jest.fn(response => response).mockName(`${id}.onResponse`), + onResponseError: jest.fn(reject).mockName(`${id}.onResponseError`), }); } } diff --git a/test/specs/cancellation.spec.js b/test/specs/cancellation.spec.js new file mode 100644 index 0000000..1ba5d30 --- /dev/null +++ b/test/specs/cancellation.spec.js @@ -0,0 +1,68 @@ +import { Service } from '../../dist/axios-middleware.common'; +import MiddlewareMock from '../mocks/MiddlewareMock'; + +describe('onRequest cancellation', () => { + it('can reject a request', () => { + const service = new Service(); + service.originalAdapter = jest.fn(conf => Promise.resolve(conf)); + + const middleware1 = new MiddlewareMock('middleware1'); + const middleware2 = new MiddlewareMock('middleware2'); + + middleware1.onRequest.mockImplementation(() => Promise.reject()); + + service.register([ + middleware2, + middleware1, + ]); + + return service.adapter('test').catch(() => { + expect(middleware1.onRequest).toBeCalledTimes(1); + expect(middleware2.onRequest).not.toBeCalled(); + + expect(middleware1.onRequestError).not.toBeCalled(); + expect(middleware2.onRequestError).toBeCalledTimes(1); + + expect(middleware1.onSync).not.toBeCalled(); + expect(middleware2.onSync).not.toBeCalled(); + + expect(middleware2.onResponse).not.toBeCalled(); + expect(middleware1.onResponse).not.toBeCalled(); + + expect(middleware2.onResponseError).toBeCalledTimes(1); + expect(middleware1.onResponseError).toBeCalledTimes(1); + }); + }); + + it('can cancel a request by returning false', () => { + const service = new Service(); + service.originalAdapter = jest.fn(conf => Promise.resolve(conf)); + + const middleware1 = new MiddlewareMock(1); + const middleware2 = new MiddlewareMock(2); + + middleware1.onRequest.mockImplementation(() => false); + + service.register([ + middleware2, + middleware1, + ]); + + return service.adapter('test').catch(() => { + expect(middleware1.onRequest).toBeCalledTimes(1); + expect(middleware2.onRequest).not.toBeCalled(); + + expect(middleware1.onRequestError).not.toBeCalled(); + expect(middleware2.onRequestError).not.toBeCalled(); + + expect(middleware1.onSync).not.toBeCalled(); + expect(middleware2.onSync).not.toBeCalled(); + + expect(middleware2.onResponse).not.toBeCalled(); + expect(middleware1.onResponse).not.toBeCalled(); + + expect(middleware2.onResponseError).not.toBeCalled(); + expect(middleware1.onResponseError).not.toBeCalled(); + }); + }); +}); diff --git a/test/specs/service.spec.js b/test/specs/service.spec.js index d0438cc..cabf6f3 100644 --- a/test/specs/service.spec.js +++ b/test/specs/service.spec.js @@ -1,7 +1,6 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Service } from '../../dist/axios-middleware.common'; -import MiddlewareMock from '../mocks/MiddlewareMock'; const http = axios.create(); const mock = new MockAdapter(http); @@ -18,32 +17,6 @@ describe('Middleware service', () => { mock.restore(); }); - it('throws when adding the same middleware instance', () => { - const middleware = {}; - - service.register(middleware); - - expect(() => service.register(middleware)).toThrow(); - }); - - it('works with both middleware syntaxes', () => { - expect.assertions(2); - const middleware = new MiddlewareMock(); - const simplifiedSyntax = { - onRequest: jest.fn(config => config), - }; - - service.register([ - middleware, - simplifiedSyntax, - ]); - - service.adapter().then(() => { - expect(middleware.onRequest).toHaveBeenCalled(); - expect(simplifiedSyntax.onRequest).toHaveBeenCalled(); - }); - }); - it('runs the middlewares in order', () => { expect.assertions(1); diff --git a/test/specs/syntax.spec.js b/test/specs/syntax.spec.js new file mode 100644 index 0000000..879b8a4 --- /dev/null +++ b/test/specs/syntax.spec.js @@ -0,0 +1,34 @@ +import { Service } from '../../dist/axios-middleware.common'; +import MiddlewareMock from '../mocks/MiddlewareMock'; + +describe('Middleware syntax', () => { + it('throws when adding the same middleware instance', () => { + const service = new Service(); + const middleware = {}; + + service.register(middleware); + + expect(() => service.register(middleware)).toThrow(); + }); + + it('works with both middleware syntaxes', () => { + expect.assertions(2); + const service = new Service(); + service.originalAdapter = jest.fn(conf => Promise.resolve(conf)); + + const middleware = new MiddlewareMock(); + const simplifiedSyntax = { + onRequest: jest.fn(config => config), + }; + + service.register([ + middleware, + simplifiedSyntax, + ]); + + return service.adapter({}).then(() => { + expect(middleware.onRequest).toHaveBeenCalled(); + expect(simplifiedSyntax.onRequest).toHaveBeenCalled(); + }); + }); +});