diff --git a/README.md b/README.md index 1c454f0..f6b5f80 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Middlewares - `forceRetry` - function(cb, delay), when request is delayed for next retry, middleware will call this function and pass to it a callback and delay time. When you call this callback, middleware will proceed request immediately (default: `false`). - **authMiddleware** - for adding auth token, and refreshing it if gets 401 response from server. - `token` - string or function(req) which returns token. If function is provided, then it will be called for every request (so you may change tokens on fly). - - `tokenRefreshPromise`: - function(req, err) which must return promise with new token, called only if server returns 401 status code and this function is provided. + - `tokenRefreshPromise`: - function(req, err) which must return promise or regular value with a new token. This function is called when server returns 401 status code. After receiving a new token, middleware re-run query to the server with it seamlessly for Relay. - `allowEmptyToken` - allow made a request without Authorization header if token is empty (default: `false`). - `prefix` - prefix before token (default: `'Bearer '`). - `header` - name of the HTTP header to pass the token in (default: `'Authorization'`). diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 837018c..4005cc0 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -38,18 +38,26 @@ export default function authMiddleware(opts = {}) { } return next(req); }) - .then(res => { - if (res.status === 401 && tokenRefreshPromise) { - throw new WrongTokenError('Received status 401 from server', res); + .catch(err => { + if ( + err && + err.fetchResponse && + err.fetchResponse.status === 401 && + tokenRefreshPromise + ) { + throw new WrongTokenError( + 'Received status 401 from server', + err.fetchResponse + ); + } else { + throw err; } - return res; }) .catch(err => { if (err.name === 'WrongTokenError') { if (!tokenRefreshInProgress) { - tokenRefreshInProgress = tokenRefreshPromise( - req, - err.res + tokenRefreshInProgress = Promise.resolve( + tokenRefreshPromise(req, err.res) ).then(newToken => { tokenRefreshInProgress = null; return newToken; diff --git a/test/middleware/auth.test.js b/test/middleware/auth.test.js new file mode 100644 index 0000000..c0ca0a1 --- /dev/null +++ b/test/middleware/auth.test.js @@ -0,0 +1,149 @@ +import { assert } from 'chai'; +import fetchMock from 'fetch-mock'; +import { RelayNetworkLayer } from '../../src'; +import { mockReq } from '../testutils'; +import authMiddleware from '../../src/middleware/auth'; + +describe('Middleware / auth', () => { + describe('`token` option as string (with default `prefix` and `header`)', () => { + const rnl = new RelayNetworkLayer([ + authMiddleware({ + token: '123', + tokenRefreshPromise: () => 345, + }), + ]); + + beforeEach(() => { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '/graphql', + response: { + status: 200, + body: { data: 'PAYLOAD' }, + sendAsJson: true, + }, + method: 'POST', + }); + }); + + it('should work with query', () => { + const req1 = mockReq(); + return rnl.sendQueries([req1]).then(() => { + assert.equal(req1.payload.response, 'PAYLOAD'); + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 1); + assert.equal(reqs[0][1].headers.Authorization, 'Bearer 123'); + }); + }); + + it('should work with mutation', () => { + const req1 = mockReq(); + return assert.isFulfilled( + rnl.sendMutation(req1).then(() => { + assert.equal(req1.payload.response, 'PAYLOAD'); + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 1); + assert.equal(reqs[0][1].headers.Authorization, 'Bearer 123'); + }) + ); + }); + }); + + describe('`token` option as thunk (with custom `prefix` and `header`)', () => { + const rnl = new RelayNetworkLayer([ + authMiddleware({ + token: () => '333', + tokenRefreshPromise: () => 345, + prefix: 'MyBearer ', + header: 'MyAuthorization', + }), + ]); + + beforeEach(() => { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '/graphql', + response: { + status: 200, + body: { data: 'PAYLOAD' }, + sendAsJson: true, + }, + method: 'POST', + }); + }); + + it('should work with query', () => { + const req1 = mockReq(); + return rnl.sendQueries([req1]).then(() => { + assert.equal(req1.payload.response, 'PAYLOAD'); + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 1); + assert.equal(reqs[0][1].headers.MyAuthorization, 'MyBearer 333'); + }); + }); + + it('should work with mutation', () => { + const req1 = mockReq(); + return assert.isFulfilled( + rnl.sendMutation(req1).then(() => { + assert.equal(req1.payload.response, 'PAYLOAD'); + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 1); + assert.equal(reqs[0][1].headers.MyAuthorization, 'MyBearer 333'); + }) + ); + }); + }); + + describe('`tokenRefreshPromise` should be called on 401 response', () => { + beforeEach(() => { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '/graphql', + response: { + status: 401, + body: { data: 'PAYLOAD' }, + sendAsJson: true, + }, + method: 'POST', + }); + }); + + it('should work with query (provided promise)', () => { + const rnl = new RelayNetworkLayer([ + authMiddleware({ + token: '123', + tokenRefreshPromise: () => Promise.resolve(345), + }), + ]); + + const req1 = mockReq(); + return rnl.sendQueries([req1]).then(() => { + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 2); + assert.equal(reqs[1][1].headers.Authorization, 'Bearer 345'); + }); + }); + + it('should work with mutation (provided regular value)', () => { + const rnl = new RelayNetworkLayer([ + authMiddleware({ + token: '123', + tokenRefreshPromise: () => 456, + }), + ]); + + const req1 = mockReq(); + return assert.isFulfilled( + rnl.sendMutation(req1).then(() => { + const reqs = fetchMock.calls('/graphql'); + assert.equal(reqs.length, 2); + assert.equal(reqs[1][1].headers.Authorization, 'Bearer 456'); + }) + ); + }); + }); +}); diff --git a/test/middleware/url.test.js b/test/middleware/url.test.js index 2e69266..e32ebeb 100644 --- a/test/middleware/url.test.js +++ b/test/middleware/url.test.js @@ -5,7 +5,7 @@ import { mockReq } from '../testutils'; import urlMiddleware from '../../src/middleware/url'; describe('Middleware / url', () => { - describe('url option as string', () => { + describe('`url` option as string', () => { const rnl = new RelayNetworkLayer([ urlMiddleware({ url: '/other/url', @@ -28,7 +28,7 @@ describe('Middleware / url', () => { it('should work with query', () => { const req1 = mockReq(); - rnl.sendQueries([req1]).then(() => { + return rnl.sendQueries([req1]).then(() => { assert.equal(req1.payload.response, 'PAYLOAD'); }); }); @@ -43,7 +43,7 @@ describe('Middleware / url', () => { }); }); - describe('url option as thunk', () => { + describe('`url` option as thunk', () => { const rnl = new RelayNetworkLayer([ urlMiddleware({ url: (_) => '/thunk_url', // eslint-disable-line @@ -66,7 +66,7 @@ describe('Middleware / url', () => { it('should work with query', () => { const req1 = mockReq(); - rnl.sendQueries([req1]).then(() => { + return rnl.sendQueries([req1]).then(() => { assert.equal(req1.payload.response, 'PAYLOAD'); }); });