diff --git a/src/components/user/factory.js b/src/components/user/factory.js index 70788b6..2c471fa 100644 --- a/src/components/user/factory.js +++ b/src/components/user/factory.js @@ -4,6 +4,8 @@ import request from 'services/request'; import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware'; import refreshTokenMiddleware from './middlewares/refreshTokenMiddleware'; +let promise; + /** * Initializes User state with the fresh data * @@ -12,10 +14,14 @@ import refreshTokenMiddleware from './middlewares/refreshTokenMiddleware'; * @return {Promise} - a promise, that resolves in User state */ export function factory(store) { + if (promise) { + return promise; + } + request.addMiddleware(refreshTokenMiddleware(store)); request.addMiddleware(bearerHeaderMiddleware(store)); - return new Promise((resolve, reject) => { + promise = new Promise((resolve, reject) => { const {user} = store.getState(); if (user.token) { @@ -26,4 +32,6 @@ export function factory(store) { // auto-detect guests language store.dispatch(changeLang(user.lang)).then(resolve, reject); }); + + return promise; } diff --git a/src/components/user/middlewares/refreshTokenMiddleware.js b/src/components/user/middlewares/refreshTokenMiddleware.js index 712c684..84ce2c4 100644 --- a/src/components/user/middlewares/refreshTokenMiddleware.js +++ b/src/components/user/middlewares/refreshTokenMiddleware.js @@ -16,7 +16,7 @@ export default function refreshTokenMiddleware({dispatch, getState}) { const {isGuest, refreshToken, token} = getState().user; const isRefreshTokenRequest = data.url.includes('refresh-token'); - if (isGuest || isRefreshTokenRequest || !token) { + if (isGuest || isRefreshTokenRequest) { return data; } @@ -65,14 +65,14 @@ export default function refreshTokenMiddleware({dispatch, getState}) { function requestAccessToken(refreshToken, dispatch) { let promise; if (refreshToken) { - promise = authentication.refreshToken(refreshToken); + promise = authentication.requestToken(refreshToken); } else { promise = Promise.reject(); } return promise - .then((resp) => dispatch(updateUser({ - token: resp.access_token + .then(({token}) => dispatch(updateUser({ + token }))) .catch(() => dispatch(logout())); } diff --git a/src/services/api/authentication.js b/src/services/api/authentication.js index 3f9320c..52e5fa7 100644 --- a/src/services/api/authentication.js +++ b/src/services/api/authentication.js @@ -36,10 +36,19 @@ export default { ); }, - refreshToken(refresh_token) { + /** + * Request new access token using a refreshToken + * + * @param {string} refreshToken + * + * @return {Promise} - resolves to {token} + */ + requestToken(refreshToken) { return request.post( '/api/authentication/refresh-token', - {refresh_token} - ); + {refresh_token: refreshToken} + ).then((resp) => ({ + token: resp.access_token + })); } }; diff --git a/tests/components/user/middlewares/bearerHeaderMiddleware.test.js b/tests/components/user/middlewares/bearerHeaderMiddleware.test.js new file mode 100644 index 0000000..0d2ea5e --- /dev/null +++ b/tests/components/user/middlewares/bearerHeaderMiddleware.test.js @@ -0,0 +1,44 @@ +import expect from 'unexpected'; + +import bearerHeaderMiddleware from 'components/user/middlewares/bearerHeaderMiddleware'; + +describe('bearerHeaderMiddleware', () => { + it('should set Authorization header', () => { + const token = 'foo'; + const middleware = bearerHeaderMiddleware({ + getState: () => ({ + user: {token} + }) + }); + + const data = { + options: { + headers: {} + } + }; + + middleware.before(data); + + expect(data.options.headers, 'to satisfy', { + Authorization: `Bearer ${token}` + }); + }); + + it('should not set Authorization header if no token', () => { + const middleware = bearerHeaderMiddleware({ + getState: () => ({ + user: {} + }) + }); + + const data = { + options: { + headers: {} + } + }; + + middleware.before(data); + + expect(data.options.headers.Authorization, 'to be undefined'); + }); +}); diff --git a/tests/components/user/middlewares/refreshTokenMiddleware.test.js b/tests/components/user/middlewares/refreshTokenMiddleware.test.js new file mode 100644 index 0000000..c8c73bb --- /dev/null +++ b/tests/components/user/middlewares/refreshTokenMiddleware.test.js @@ -0,0 +1,154 @@ +import expect from 'unexpected'; + +import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware'; + +import authentication from 'services/api/authentication'; + +const refreshToken = 'foo'; +const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8'; +// valid till 2100 year +const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE5NzcsImV4cCI6NDEwMjQ0NDgwMCwiaWF0IjoxNDcwNzYxOTc3LCJqdGkiOiJpZDEyMzQ1NiJ9.M4KY4QgHOUzhpAZjWoHJbGsEJPR-RBsJ1c1BKyxvAoU'; + +describe('refreshTokenMiddleware', () => { + let middleware; + let getState; + let dispatch; + + beforeEach(() => { + sinon.stub(authentication, 'requestToken').named('authentication.requestToken'); + + getState = sinon.stub().named('store.getState'); + dispatch = sinon.stub().named('store.dispatch'); + + middleware = refreshTokenMiddleware({getState, dispatch}); + }); + + afterEach(() => { + authentication.requestToken.restore(); + }); + + describe('#before', () => { + it('should request new token', () => { + getState.returns({ + user: { + token: expiredToken, + refreshToken, + isGuest: false + } + }); + + const data = { + url: 'foo', + options: { + headers: {} + } + }; + + authentication.requestToken.returns(Promise.resolve({token: validToken})); + + return middleware.before(data).then((resp) => { + expect(resp, 'to satisfy', data); + + expect(authentication.requestToken, 'to have a call satisfying', [ + refreshToken + ]); + }); + }); + + it('should not be applied for guests', () => { + getState.returns({ + user: { + isGuest: true + } + }); + + authentication.requestToken.returns(Promise.resolve({token: validToken})); + + const data = {url: 'foo'}; + const resp = middleware.before(data); + + expect(resp, 'to satisfy', data); + + expect(authentication.requestToken, 'was not called'); + }); + + it('should not apply to refresh-token request', () => { + getState.returns({ + user: {} + }); + + authentication.requestToken.returns(Promise.resolve({token: validToken})); + + const data = {url: '/refresh-token'}; + const resp = middleware.before(data); + + expect(resp, 'to satisfy', data); + + expect(authentication.requestToken, 'was not called'); + }); + + xit('should update user with new token'); // TODO: need a way to test, that action was called + + xit('should logout if token request failed', () => { + getState.returns({ + user: { + token: expiredToken, + refreshToken, + isGuest: false + } + }); + + authentication.requestToken.returns(Promise.reject()); + + return middleware.before({url: 'foo'}).then((resp) => { + // TODO: need a way to test, that action was called + expect(dispatch, 'to have a call satisfying', logout); + }); + }); + }); + + describe('#catch', () => { + it('should request new token', () => { + getState.returns({ + user: { + refreshToken + } + }); + + const restart = sinon.stub().named('restart'); + + const data = { + url: 'foo', + options: { + headers: {} + } + }; + + authentication.requestToken.returns(Promise.resolve({token: validToken})); + + return middleware.catch({ + status: 401, + message: 'Token expired' + }, restart).then(() => { + expect(authentication.requestToken, 'to have a call satisfying', [ + refreshToken + ]); + expect(restart, 'was called'); + }); + }); + + xit('should logout user if token cannot be refreshed'); // TODO: need a way to test, that action was called + + it('should pass the rest of failed requests through', () => { + const resp = {}; + + const promise = middleware.catch(resp); + + expect(promise, 'to be rejected'); + + return promise.catch((actual) => { + expect(actual, 'to be', resp); + }); + }); + }); +});