From 5bb39f32796af51eba9c01f5bc5be5d88e8c340f Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Tue, 2 Dec 2014 15:45:19 -0700 Subject: [PATCH] Initial interceptor implementation. --- karma.conf.js | 5 +- lib/axios.js | 64 +++++-- test/specs/interceptors.spec.js | 299 ++++++++++++++++++++++++++++++++ test/specs/options.spec.js | 94 +++++++--- test/specs/promise.spec.js | 66 ++++--- test/specs/transform.spec.js | 104 +++++++---- test/specs/wrapper.spec.js | 175 +++++++++++++------ test/specs/xsrf.spec.js | 41 ++++- 8 files changed, 680 insertions(+), 168 deletions(-) create mode 100644 test/specs/interceptors.spec.js diff --git a/karma.conf.js b/karma.conf.js index 7022baf..c706c56 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -16,20 +16,21 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ 'dist/axios.js', + 'node_modules/es6-promise/dist/promise-1.0.0.js', 'test/specs/**/*.spec.js' ], // list of files to exclude exclude: [ - + ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - + }, diff --git a/lib/axios.js b/lib/axios.js index 93f1306..2a6a2e5 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -13,20 +13,22 @@ var axios = module.exports = function axios(config) { // Don't allow overriding defaults.withCredentials config.withCredentials = config.withCredentials || defaults.withCredentials; - var promise = new Promise(function (resolve, reject) { - try { - // For browsers use XHR adapter - if (typeof window !== 'undefined') { - require('./adapters/xhr')(resolve, reject, config); + var serverRequest = function (config) { + return new Promise(function (resolve, reject) { + try { + // For browsers use XHR adapter + if (typeof window !== 'undefined') { + require('./adapters/xhr')(resolve, reject, config); + } + // For node use HTTP adapter + else if (typeof process !== 'undefined') { + require('./adapters/http')(resolve, reject, config); + } + } catch (e) { + reject(e); } - // For node use HTTP adapter - else if (typeof process !== 'undefined') { - require('./adapters/http')(resolve, reject, config); - } - } catch (e) { - reject(e); - } - }); + }); + }; function deprecatedMethod(method, instead, docs) { try { @@ -41,6 +43,24 @@ var axios = module.exports = function axios(config) { } catch (e) {} } + var chain = [serverRequest, undefined]; + var promise = Promise.resolve(config); + + utils.forEach(axios.interceptors.request.handlers, function (interceptor) { + chain.unshift(interceptor.request, interceptor.requestError); + }); + + utils.forEach(axios.interceptors.response.handlers, function (interceptor) { + chain.push(interceptor.response, interceptor.responseError); + }); + + while (chain.length) { + var thenFn = chain.shift(); + var rejectFn = chain.shift(); + + promise = promise.then(thenFn, rejectFn); + } + // Provide alias for success promise.success = function success(fn) { deprecatedMethod('success', 'then', 'https://github.com/mzabriskie/axios/blob/master/README.md#response-api'); @@ -73,6 +93,22 @@ axios.all = function (promises) { }; axios.spread = require('./helpers/spread'); +// interceptors +axios.interceptors = { + request: { + handlers: [], + use: function (thenFn, rejectFn) { + axios.interceptors.request.handlers.push({ request: thenFn, requestError: rejectFn }); + } + }, + response: { + handlers: [], + use: function (thenFn, rejectFn) { + axios.interceptors.response.handlers.push({ response: thenFn, responseError: rejectFn }); + } + } +}; + // Provide aliases for supported request methods createShortMethods('delete', 'get', 'head'); createShortMethodsWithData('post', 'put', 'patch'); @@ -98,4 +134,4 @@ function createShortMethodsWithData() { })); }; }); -} \ No newline at end of file +} diff --git a/test/specs/interceptors.spec.js b/test/specs/interceptors.spec.js new file mode 100644 index 0000000..8c7db25 --- /dev/null +++ b/test/specs/interceptors.spec.js @@ -0,0 +1,299 @@ +describe('interceptors', function () { + beforeEach(function () { + jasmine.Ajax.install(); + }); + + afterEach(function () { + jasmine.Ajax.uninstall(); + axios.interceptors.request.handlers = []; + axios.interceptors.response.handlers = []; + }); + + it('should add a request interceptor', function () { + var request; + + runs(function () { + axios.interceptors.request.use(function (config) { + config.headers.test = 'added by interceptor'; + return config; + }); + + axios({ + url: '/foo' + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + + expect(request.requestHeaders.test).toBe('added by interceptor'); + }); + }); + + it('should add a request interceptor that returns a new config object', function () { + var request; + + runs(function () { + axios.interceptors.request.use(function () { + return { + url: '/bar', + method: 'post' + }; + }); + + axios({ + url: '/foo' + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + + expect(request.method).toBe('post'); + expect(request.url).toBe('/bar'); + }); + }); + + it('should add a request interceptor that returns a promise', function () { + var request; + + runs(function () { + axios.interceptors.request.use(function (config) { + return new Promise(function (resolve) { + // do something async + setTimeout(function () { + config.headers.async = 'promise'; + resolve(config); + }, 10); + }); + }); + + axios({ + url: '/foo' + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + + expect(request.requestHeaders.async).toBe('promise'); + }); + }); + + it('should add multiple request interceptors', function () { + var request; + + runs(function () { + axios.interceptors.request.use(function (config) { + config.headers.test1 = '1'; + return config; + }); + axios.interceptors.request.use(function (config) { + config.headers.test2 = '2'; + return config; + }); + axios.interceptors.request.use(function (config) { + config.headers.test3 = '3'; + return config; + }); + + axios({ + url: '/foo' + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + + expect(request.requestHeaders.test1).toBe('1'); + expect(request.requestHeaders.test2).toBe('2'); + expect(request.requestHeaders.test3).toBe('3'); + }); + }); + + it('should add a response interceptor', function () { + var request, response; + + runs(function () { + axios.interceptors.response.use(function (data) { + data.data = data.data + ' - modified by interceptor'; + return data; + }); + + axios({ + url: '/foo' + }).then(function (data) { + response = data; + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + }); + + waitsFor(function () { + return response; + }, 'waiting for the response', 100); + + runs(function () { + expect(response.data).toBe('OK - modified by interceptor'); + }); + }); + + it('should add a response interceptor that returns a new data object', function () { + var request, response; + + runs(function () { + axios.interceptors.response.use(function () { + return { + data: 'stuff' + }; + }); + + axios({ + url: '/foo' + }).then(function (data) { + response = data; + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + }); + + waitsFor(function () { + return response; + }, 'waiting for the response', 100); + + runs(function () { + expect(response.data).toBe('stuff'); + }); + }); + + it('should add a response interceptor that returns a promise', function () { + var request, response; + + runs(function () { + axios.interceptors.response.use(function (data) { + return new Promise(function (resolve) { + // do something async + setTimeout(function () { + data.data = 'you have been promised!'; + resolve(data); + }, 10); + }); + }); + + axios({ + url: '/foo' + }).then(function (data) { + response = data; + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + }); + + waitsFor(function () { + return response; + }, 'waiting for the response', 100); + + runs(function () { + expect(response.data).toBe('you have been promised!'); + }); + }); + + it('should add multiple response interceptors', function () { + var request, response; + + runs(function () { + axios.interceptors.response.use(function (data) { + data.data = data.data + '1'; + return data; + }); + axios.interceptors.response.use(function (data) { + data.data = data.data + '2'; + return data; + }); + axios.interceptors.response.use(function (data) { + data.data = data.data + '3'; + return data; + }); + + axios({ + url: '/foo' + }).then(function (data) { + response = data; + }); + }); + + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: 'OK' + }); + }); + + waitsFor(function () { + return response; + }, 'waiting for the response', 100); + + runs(function () { + expect(response.data).toBe('OK123'); + }); + }); +}); diff --git a/test/specs/options.spec.js b/test/specs/options.spec.js index 154f729..4a9fb1f 100644 --- a/test/specs/options.spec.js +++ b/test/specs/options.spec.js @@ -3,49 +3,89 @@ describe('options', function () { jasmine.Ajax.install(); }); + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + it('should default method to get', function () { - axios({ - url: '/foo' + var request; + + runs(function () { + axios({ + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.method).toBe('get'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.method).toBe('get'); + }); }); it('should accept headers', function () { - axios({ - url: '/foo', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } + var request; + + runs(function () { + axios({ + url: '/foo', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders['X-Requested-With']).toEqual('XMLHttpRequest'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders['X-Requested-With']).toEqual('XMLHttpRequest'); + }); }); it('should accept params', function () { - axios({ - url: '/foo', - params: { - foo: 123, - bar: 456 - } + var request; + + runs(function () { + axios({ + url: '/foo', + params: { + foo: 123, + bar: 456 + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.url).toBe('/foo?foo=123&bar=456'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.url).toBe('/foo?foo=123&bar=456'); + }); }); it('should allow overriding default headers', function () { - axios({ - url: '/foo', - headers: { - 'Accept': 'foo/bar' - } + var request; + + runs(function () { + axios({ + url: '/foo', + headers: { + 'Accept': 'foo/bar' + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders['Accept']).toEqual('foo/bar'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders['Accept']).toEqual('foo/bar'); + }); }); -}); \ No newline at end of file +}); diff --git a/test/specs/promise.spec.js b/test/specs/promise.spec.js index 9369497..8946aed 100644 --- a/test/specs/promise.spec.js +++ b/test/specs/promise.spec.js @@ -3,27 +3,36 @@ describe('promise', function () { jasmine.Ajax.install(); }); + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + it('should provide succinct object to then', function () { - var response; - var fulfilled = false; + var request, response; - axios({ - url: '/foo' - }).then(function (r) { + runs(function () { + axios({ + url: '/foo' + }).then(function (r) { response = r; - fulfilled = true; }); - - var request = jasmine.Ajax.requests.mostRecent(); - request.response({ - status: 200, - responseText: '{"hello":"world"}' }); waitsFor(function () { - return fulfilled; + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: '{"hello":"world"}' + }); }); + waitsFor(function () { + return response; + }, 'waiting for the response', 100); + runs(function () { expect(typeof response).toEqual('object'); expect(response.data.hello).toEqual('world'); @@ -34,32 +43,35 @@ describe('promise', function () { }); it('should provide verbose arguments to success', function () { - var data; - var status; - var headers; - var config; - var fulfilled = false; + var request, data, status, headers, config; - axios({ - url: '/foo' - }).success(function (d, s, h, c) { + runs(function () { + axios({ + url: '/foo' + }).success(function (d, s, h, c) { data = d; status = s; headers = h; config = c; fulfilled = true; }); - - var request = jasmine.Ajax.requests.mostRecent(); - request.response({ - status: 200, - responseText: '{"hello":"world"}' }); waitsFor(function () { - return fulfilled; + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + request.response({ + status: 200, + responseText: '{"hello":"world"}' + }); }); + waitsFor(function () { + return data; + }, 'waiting for the data', 100); + runs(function () { expect(data.hello).toEqual('world'); expect(status).toBe(200); @@ -102,4 +114,4 @@ describe('promise', function () { expect(sum).toEqual(123 + 456); }); }); -}); \ No newline at end of file +}); diff --git a/test/specs/transform.spec.js b/test/specs/transform.spec.js index 0d1ada9..28dbd50 100644 --- a/test/specs/transform.spec.js +++ b/test/specs/transform.spec.js @@ -3,70 +3,106 @@ describe('transform', function () { jasmine.Ajax.install(); }); + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + it('should transform JSON to string', function () { + var request; var data = { foo: 'bar' }; - axios({ - url: '/foo', - method: 'post', - data: data + runs(function () { + axios({ + url: '/foo', + method: 'post', + data: data + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.params).toEqual('{"foo":"bar"}'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.params).toEqual('{"foo":"bar"}'); + }); }); it('should override default transform', function () { + var request; var data = { foo: 'bar' }; - axios({ - url: '/foo', - method: 'post', - data: data, - transformRequest: function (data) { - return data; - } + runs(function () { + axios({ + url: '/foo', + method: 'post', + data: data, + transformRequest: function (data) { + return data; + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(typeof request.params).toEqual('object'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(typeof request.params).toEqual('object'); + }); }); it('should allow an Array of transformers', function () { + var request; var data = { foo: 'bar' }; - axios({ - url: '/foo', - method: 'post', - data: data, - transformRequest: axios.defaults.transformRequest.concat( - function (data) { - return data.replace('bar', 'baz'); - } - ) + runs(function () { + axios({ + url: '/foo', + method: 'post', + data: data, + transformRequest: axios.defaults.transformRequest.concat( + function (data) { + return data.replace('bar', 'baz'); + } + ) + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.params).toEqual('{"foo":"baz"}'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.params).toEqual('{"foo":"baz"}'); + }); }); it('should allowing mutating headers', function () { var token = Math.floor(Math.random() * Math.pow(2, 64)).toString(36); + var request; - axios({ - url: '/foo', - transformRequest: function (data, headers) { - headers['X-Authorization'] = token; - } + runs(function () { + axios({ + url: '/foo', + transformRequest: function (data, headers) { + headers['X-Authorization'] = token; + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders['X-Authorization']).toEqual(token); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders['X-Authorization']).toEqual(token); + }); }); -}); \ No newline at end of file +}); diff --git a/test/specs/wrapper.spec.js b/test/specs/wrapper.spec.js index 7ae2cfa..cbcfa08 100644 --- a/test/specs/wrapper.spec.js +++ b/test/specs/wrapper.spec.js @@ -3,102 +3,167 @@ describe('wrapper', function () { jasmine.Ajax.install(); }); + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + it('should make an http request', function () { - axios({ - url: '/foo' + var request; + + runs(function () { + axios({ + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.url).toBe('/foo'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.url).toBe('/foo') + }); }); it('should default common headers', function () { - axios({ - url: '/foo' + var request; + var headers = axios.defaults.headers.common; + + runs(function () { + axios({ + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - var headers = axios.defaults.headers.common; - for (var key in headers) { - if (headers.hasOwnProperty(key)) { - expect(request.requestHeaders[key]).toEqual(headers[key]); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + expect(request.requestHeaders[key]).toEqual(headers[key]); + } } - } + }); }); it('should add extra headers for post', function () { - axios({ - method: 'post', - url: '/foo', - data: 'fizz=buzz' + var request; + var headers = axios.defaults.headers.common; + + runs(function () { + axios({ + method: 'post', + url: '/foo', + data: 'fizz=buzz' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - var headers = axios.defaults.headers.post; - for (var key in headers) { - if (headers.hasOwnProperty(key)) { - expect(request.requestHeaders[key]).toEqual(headers[key]); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + expect(request.requestHeaders[key]).toEqual(headers[key]); + } } - } + }); }); it('should use application/json when posting an object', function () { - axios({ - url: '/foo/bar', - method: 'post', - data: { - firstName: 'foo', - lastName: 'bar' - } + var request; + + runs(function () { + axios({ + url: '/foo/bar', + method: 'post', + data: { + firstName: 'foo', + lastName: 'bar' + } + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders['Content-Type']).toEqual('application/json;charset=utf-8'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders['Content-Type']).toEqual('application/json;charset=utf-8'); + }); }); it('should support binary data as array buffer', function () { + var request; var input = new Int8Array(2); input[0] = 1; input[1] = 2; - axios({ - method: 'post', - url: '/foo', - data: input.buffer + runs(function () { + axios({ + method: 'post', + url: '/foo', + data: input.buffer + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - var output = new Int8Array(request.params.buffer); - expect(output.length).toEqual(2); - expect(output[0]).toEqual(1); - expect(output[1]).toEqual(2); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + var output = new Int8Array(request.params.buffer); + expect(output.length).toEqual(2); + expect(output[0]).toEqual(1); + expect(output[1]).toEqual(2); + }); }); it('should support binary data as array buffer view', function () { + var request; var input = new Int8Array(2); input[0] = 1; input[1] = 2; - axios({ - method: 'post', - url: '/foo', - data: input + runs(function () { + axios({ + method: 'post', + url: '/foo', + data: input + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - var output = new Int8Array(request.params.buffer); - expect(output.length).toEqual(2); - expect(output[0]).toEqual(1); - expect(output[1]).toEqual(2); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + var output = new Int8Array(request.params.buffer); + expect(output.length).toEqual(2); + expect(output[0]).toEqual(1); + expect(output[1]).toEqual(2); + }); }); it('should remove content-type if data is empty', function () { - axios({ - method: 'post', - url: '/foo' + var request; + + runs(function () { + axios({ + method: 'post', + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders['content-type']).toEqual(undefined); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders['content-type']).toEqual(undefined); + }); }); -}); \ No newline at end of file +}); diff --git a/test/specs/xsrf.spec.js b/test/specs/xsrf.spec.js index ddfbbcd..0c3a3d4 100644 --- a/test/specs/xsrf.spec.js +++ b/test/specs/xsrf.spec.js @@ -1,24 +1,47 @@ describe('xsrf', function () { + beforeEach(function () { + jasmine.Ajax.install(); + }); + afterEach(function () { document.cookie = axios.defaults.xsrfCookieName + '=;expires=' + new Date(Date.now() - 86400000).toGMTString(); + jasmine.Ajax.uninstall(); }); it('should not set xsrf header if cookie is null', function () { - axios({ - url: '/foo' + var request; + + runs(function () { + axios({ + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(undefined); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(undefined); + }); }); it('should set xsrf header if cookie is set', function () { + var request; document.cookie = axios.defaults.xsrfCookieName + '=12345'; - axios({ - url: '/foo' + + runs(function () { + axios({ + url: '/foo' + }); }); - var request = jasmine.Ajax.requests.mostRecent(); - expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual('12345'); + waitsFor(function () { + return request = jasmine.Ajax.requests.mostRecent(); + }, 'waiting for the request', 100); + + runs(function () { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual('12345'); + }); }); -}); \ No newline at end of file +});