import expect from 'app/test/unexpected'; import sinon from 'sinon'; import refreshTokenMiddleware from 'app/components/user/middlewares/refreshTokenMiddleware'; import { browserHistory } from 'app/services/history'; import * as authentication from 'app/services/api/authentication'; import { InternalServerError } from 'app/services/request'; import { updateToken } from 'app/components/accounts/actions'; 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; const email = 'test@email.com'; beforeEach(() => { sinon .stub(authentication, 'requestToken') .named('authentication.requestToken'); sinon.stub(authentication, 'logout').named('authentication.logout'); sinon.stub(browserHistory, 'push'); getState = sinon.stub().named('store.getState'); dispatch = sinon .spy(arg => (typeof arg === 'function' ? arg(dispatch, getState) : arg)) .named('store.dispatch'); middleware = refreshTokenMiddleware({ getState, dispatch } as any); }); afterEach(() => { (authentication.requestToken as any).restore(); (authentication.logout as any).restore(); (browserHistory.push as any).restore(); }); function assertRelogin() { expect(dispatch, 'to have a call satisfying', [ { type: 'auth:setCredentials', payload: { login: email, returnUrl: expect.it('to be a string'), }, }, ]); expect(browserHistory.push, 'to have a call satisfying', ['/login']); } it('must be till 2100 to test with validToken', () => expect(new Date().getFullYear(), 'to be less than', 2100)); describe('#before', () => { describe('when token expired', () => { beforeEach(() => { const account = { id: 42, email, token: expiredToken, refreshToken, }; getState.returns({ accounts: { active: account.id, available: [account], }, auth: { credentials: {}, }, user: {}, }); }); it('should request new token', () => { const data = { url: 'foo', options: { headers: {}, }, }; (authentication.requestToken as any).returns( Promise.resolve(validToken), ); return middleware.before(data).then(resp => { expect(resp, 'to satisfy', data); expect(authentication.requestToken, 'to have a call satisfying', [ refreshToken, ]); }); }); it('should not apply to refresh-token request', () => { const data = { url: '/refresh-token', options: {} }; const resp = middleware.before(data); return expect(resp, 'to be fulfilled with', data).then(() => expect(authentication.requestToken, 'was not called'), ); }); it('should not auto refresh token if options.token specified', () => { const data = { url: 'foo', options: { token: 'foo' }, }; middleware.before(data); expect(authentication.requestToken, 'was not called'); }); it('should update user with new token', () => { const data = { url: 'foo', options: { headers: {}, }, }; (authentication.requestToken as any).returns( Promise.resolve(validToken), ); return middleware .before(data) .then(() => expect(dispatch, 'to have a call satisfying', [ updateToken(validToken), ]), ); }); it('should relogin if token can not be parsed', () => { const account = { id: 42, email, token: 'realy bad token', refreshToken, }; getState.returns({ accounts: { active: account.id, available: [account], }, auth: { credentials: {}, }, user: {}, }); const req = { url: 'foo', options: {} }; return expect(middleware.before(req), 'to be rejected with', { message: 'Invalid token', }).then(() => { expect(authentication.requestToken, 'was not called'); assertRelogin(); }); }); it('should relogin if token request failed', () => { (authentication.requestToken as any).returns(Promise.reject()); return expect( middleware.before({ url: 'foo', options: {} }), 'to be rejected', ).then(() => assertRelogin()); }); it('should not logout if request failed with 5xx', () => { const resp = new InternalServerError({}, { status: 500 }); (authentication.requestToken as any).returns(Promise.reject(resp)); return expect( middleware.before({ url: 'foo', options: {} }), 'to be rejected with', resp, ).then(() => expect(dispatch, 'to have no calls satisfying', [ { payload: { isGuest: true } }, ]), ); }); }); it('should not be applied if no token', () => { getState.returns({ accounts: { active: null, available: [], }, user: {}, }); const data = { url: 'foo', options: {}, }; const resp = middleware.before(data); return expect(resp, 'to be fulfilled with', data).then(() => expect(authentication.requestToken, 'was not called'), ); }); }); describe('#catch', () => { const expiredResponse = { name: 'Unauthorized', message: 'Token expired', code: 0, status: 401, type: 'yii\\web\\UnauthorizedHttpException', }; const badTokenReponse = { name: 'Unauthorized', message: 'You are requesting with an invalid credential.', code: 0, status: 401, type: 'yii\\web\\UnauthorizedHttpException', }; const incorrectTokenReponse = { name: 'Unauthorized', message: 'Incorrect token', code: 0, status: 401, type: 'yii\\web\\UnauthorizedHttpException', }; let restart; beforeEach(() => { getState.returns({ accounts: { active: 1, available: [ { id: 1, email, token: 'old token', refreshToken, }, ], }, user: {}, }); restart = sinon.stub().named('restart'); (authentication.requestToken as any).returns(Promise.resolve(validToken)); }); function assertNewTokenRequest() { expect(authentication.requestToken, 'to have a call satisfying', [ refreshToken, ]); expect(restart, 'was called'); expect(dispatch, 'was called'); } it('should request new token if expired', () => expect( middleware.catch(expiredResponse, { options: {} }, restart), 'to be fulfilled', ).then(assertNewTokenRequest)); it('should request new token if invalid credential', () => expect( middleware.catch(badTokenReponse, { options: {} }, restart), 'to be fulfilled', ).then(assertNewTokenRequest)); it('should request new token if token is incorrect', () => expect( middleware.catch(incorrectTokenReponse, { options: {} }, restart), 'to be fulfilled', ).then(assertNewTokenRequest)); it('should relogin if no refreshToken', () => { getState.returns({ accounts: { active: 1, available: [ { id: 1, email, refreshToken: null, }, ], }, auth: { credentials: {}, }, user: {}, }); return expect( middleware.catch(incorrectTokenReponse, { options: {} }, restart), 'to be rejected', ).then(() => { assertRelogin(); }); }); it('should pass the request through if options.token specified', () => { const promise = middleware.catch( expiredResponse, { options: { token: 'foo', }, }, restart, ); return expect(promise, 'to be rejected with', expiredResponse).then( () => { expect(restart, 'was not called'); expect(authentication.requestToken, 'was not called'); }, ); }); it('should pass the rest of failed requests through', () => { const resp = {}; const promise = middleware.catch( resp, { options: {}, }, restart, ); return expect(promise, 'to be rejected with', resp).then(() => { expect(restart, 'was not called'); expect(authentication.requestToken, 'was not called'); }); }); }); });