import expect from 'app/test/unexpected'; import sinon, { SinonFakeServer } from 'sinon'; import request from 'app/services/request'; import * as authentication from 'app/services/api/authentication'; import * as accounts from 'app/services/api/accounts'; describe('authentication api', () => { let server: SinonFakeServer; beforeEach(() => { server = sinon.fakeServer.create({ autoRespond: true, }); }); afterEach(() => { server.restore(); }); describe('#login', () => { const params = { login: 'foo', password: 'secret', rememberMe: false, }; beforeEach(() => { sinon.stub(request, 'post').named('request.post'); (request.post as any).returns(Promise.resolve()); }); afterEach(() => { (request.post as any).restore(); }); it('should post to login api', () => { authentication.login(params); expect(request.post, 'to have a call satisfying', ['/api/authentication/login', params, {}]); }); it('should disable any token', () => { authentication.login(params); expect(request.post, 'to have a call satisfying', ['/api/authentication/login', params, { token: null }]); }); }); describe('#validateToken()', () => { const validToken = 'foo'; const validRefreshToken = 'bar'; const user = { id: 1 }; const validateTokenArgs: [number, string, string] = [user.id, validToken, validRefreshToken]; beforeEach(() => { sinon.stub(accounts, 'getInfo'); (accounts.getInfo as any).returns(Promise.resolve(user)); }); afterEach(() => { (accounts.getInfo as any).restore(); }); it('should request accounts.getInfo', () => expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled').then(() => { expect(accounts.getInfo, 'to have a call satisfying', [user.id, validToken]); })); it('should resolve with both tokens and user object', () => expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled with', { token: validToken, refreshToken: validRefreshToken, user, })); it('rejects if token has a bad type', () => expect(authentication.validateToken(user.id, {} as any), 'to be rejected with', 'token must be a string')); it('should allow empty refreshToken', () => expect(authentication.validateToken(user.id, 'foo', null), 'to be fulfilled')); it('rejects if accounts.getInfo request is unexpectedly failed', () => { const error = 'Something wrong'; (accounts.getInfo as any).returns(Promise.reject(error)); return expect(authentication.validateToken(...validateTokenArgs), 'to be rejected with', error); }); describe('when token is expired', () => { const expiredResponse = { name: 'Unauthorized', message: 'Token expired', code: 0, status: 401, type: 'yii\\web\\UnauthorizedHttpException', }; const newToken = 'baz'; beforeEach(() => { sinon.stub(authentication, 'requestToken'); (accounts.getInfo as any).onCall(0).returns(Promise.reject(expiredResponse)); (authentication.requestToken as any).returns(Promise.resolve(newToken)); }); afterEach(() => { (authentication.requestToken as any).restore(); }); it('resolves with new token and user object', async () => { server.respondWith( 'POST', '/api/authentication/refresh-token', JSON.stringify({ access_token: newToken, refresh_token: validRefreshToken, success: true, expires_in: 50000, }), ); await expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled with', { token: newToken, refreshToken: validRefreshToken, user, }); expect(server.requests[0].requestBody, 'to equal', `refresh_token=${validRefreshToken}`); }); it('rejects if token request failed', () => { const error = { error: 'Unexpected error example' }; server.respondWith('POST', '/api/authentication/refresh-token', [500, [], JSON.stringify(error)]); return expect(authentication.validateToken(...validateTokenArgs), 'to be rejected with', error); }); }); describe('when token is incorrect', () => { const expiredResponse = { name: 'Unauthorized', message: 'Incorrect token', code: 0, status: 401, type: 'yii\\web\\UnauthorizedHttpException', }; const newToken = 'baz'; beforeEach(() => { (accounts.getInfo as any).onCall(0).returns(Promise.reject(expiredResponse)); }); it('resolves with new token and user object', async () => { server.respondWith( 'POST', '/api/authentication/refresh-token', JSON.stringify({ access_token: newToken, refresh_token: validRefreshToken, success: true, expires_in: 50000, }), ); await expect(authentication.validateToken(...validateTokenArgs), 'to be fulfilled with', { token: newToken, refreshToken: validRefreshToken, user, }); expect(server.requests[0].requestBody, 'to equal', `refresh_token=${validRefreshToken}`); }); it('rejects if token request failed', () => { const error = { error: 'Unexpected error example' }; server.respondWith('POST', '/api/authentication/refresh-token', [500, [], JSON.stringify(error)]); return expect(authentication.validateToken(...validateTokenArgs), 'to be rejected with', error); }); }); }); describe('#logout', () => { beforeEach(() => { sinon.stub(request, 'post').named('request.post'); }); afterEach(() => { (request.post as any).restore(); }); it('should request logout api', () => { authentication.logout(); expect(request.post, 'to have a call satisfying', ['/api/authentication/logout', {}, {}]); }); it('returns a promise', () => { (request.post as any).returns(Promise.resolve()); return expect(authentication.logout(), 'to be fulfilled'); }); it('overrides token if provided', () => { const token = 'foo'; authentication.logout(token); expect(request.post, 'to have a call satisfying', ['/api/authentication/logout', {}, { token }]); }); }); describe('#requestToken', () => { const refreshToken = 'refresh-token'; beforeEach(() => { sinon.stub(request, 'post').named('request.post'); }); afterEach(() => { (request.post as any).restore(); }); it('should request refresh-token api', () => { (request.post as any).returns(Promise.resolve({})); authentication.requestToken(refreshToken); expect(request.post, 'to have a call satisfying', [ '/api/authentication/refresh-token', { refresh_token: refreshToken, // eslint-disable-line }, {}, ]); }); it('should disable bearer auth for request', () => { (request.post as any).returns(Promise.resolve({})); authentication.requestToken(refreshToken); expect(request.post, 'to have a call satisfying', [ '/api/authentication/refresh-token', { refresh_token: refreshToken, // eslint-disable-line }, { token: null }, ]); }); it('should resolve with token', () => { const token = 'token'; (request.post as any).returns( Promise.resolve({ access_token: token, // eslint-disable-line }), ); return expect(authentication.requestToken(refreshToken), 'to be fulfilled with', token); }); }); });