#48: call authentication.logout for each revoked account

This commit is contained in:
SleepWalker 2016-11-15 07:55:15 +02:00
parent 9e7d5b8338
commit 5142d65b39
7 changed files with 214 additions and 73 deletions

View File

@ -59,7 +59,10 @@ export function revoke(account) {
if (accountToReplace) { if (accountToReplace) {
return dispatch(authenticate(accountToReplace)) return dispatch(authenticate(accountToReplace))
.then(() => dispatch(remove(account))); .then(() => {
authentication.logout(account);
dispatch(remove(account));
});
} }
return dispatch(logout()); return dispatch(logout());
@ -111,8 +114,19 @@ export function activate(account) {
}; };
} }
export function logoutAll() {
return (dispatch, getState) => {
const {accounts: {available}} = getState();
available.forEach((account) => authentication.logout(account));
dispatch(reset());
};
}
export const RESET = 'accounts:reset'; export const RESET = 'accounts:reset';
/** /**
* @api private
*
* @return {object} - action definition * @return {object} - action definition
*/ */
export function reset() { export function reset() {

View File

@ -1,7 +1,7 @@
import { routeActions } from 'react-router-redux'; import { routeActions } from 'react-router-redux';
import accounts from 'services/api/accounts'; import accounts from 'services/api/accounts';
import { reset as resetAccounts } from 'components/accounts/actions'; import { logoutAll } from 'components/accounts/actions';
import authentication from 'services/api/authentication'; import authentication from 'services/api/authentication';
import { setLocale } from 'components/i18n/actions'; import { setLocale } from 'components/i18n/actions';
@ -54,24 +54,16 @@ export function setUser(payload) {
export function logout() { export function logout() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (getState().user.token) { dispatch(setUser({
authentication.logout(); lang: getState().user.lang,
} isGuest: true
}));
return new Promise((resolve) => { dispatch(logoutAll());
setTimeout(() => { // a tiny timeout to allow logout before user's token will be removed
dispatch(setUser({
lang: getState().user.lang,
isGuest: true
}));
dispatch(resetAccounts()); dispatch(routeActions.push('/login'));
dispatch(routeActions.push('/login')); return Promise.resolve();
resolve();
}, 0);
});
}; };
} }

View File

@ -13,8 +13,17 @@ const authentication = {
); );
}, },
logout() { /**
return request.post('/api/authentication/logout'); * @param {object} options
* @param {object} [options.token] - an optional token to overwrite headers
* in middleware and disable token auto-refresh
*
* @return {Promise}
*/
logout(options = {}) {
return request.post('/api/authentication/logout', {}, {
token: options.token
});
}, },
forgotPassword({ forgotPassword({

View File

@ -8,7 +8,8 @@ import {
add, ADD, add, ADD,
activate, ACTIVATE, activate, ACTIVATE,
remove, remove,
reset reset,
logoutAll
} from 'components/accounts/actions'; } from 'components/accounts/actions';
import { SET_LOCALE } from 'components/i18n/actions'; import { SET_LOCALE } from 'components/i18n/actions';
@ -116,58 +117,129 @@ describe('components/accounts/actions', () => {
}); });
describe('#revoke()', () => { describe('#revoke()', () => {
it('should switch next account if available', () => { beforeEach(() => {
sinon.stub(authentication, 'logout').named('authentication.logout');
});
afterEach(() => {
authentication.logout.restore();
});
describe('when one account available', () => {
beforeEach(() => {
getState.returns({
accounts: {
active: account,
available: [account]
},
user
});
});
it('should dispatch reset action', () =>
revoke(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
reset()
])
)
);
it('should call logout api method in background', () =>
revoke(account)(dispatch, getState).then(() =>
expect(authentication.logout, 'to have a call satisfying', [
account
])
)
);
it('should update user state', () =>
revoke(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
{payload: {isGuest: true}}
// updateUser({isGuest: true})
])
// expect(dispatch, 'to have calls satisfying', [
// [remove(account)],
// [expect.it('to be a function')]
// // [logout()] // TODO: this is not a plain action. How should we simplify its testing?
// ])
)
);
});
describe('when multiple accounts available', () => {
const account2 = {...account, id: 2}; const account2 = {...account, id: 2};
beforeEach(() => {
getState.returns({
accounts: {
active: account2,
available: [account, account2]
},
user
});
});
it('should switch to the next account', () =>
revoke(account2)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
activate(account)
])
)
);
it('should remove current account', () =>
revoke(account2)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
remove(account2)
])
)
);
it('should call logout api method in background', () =>
revoke(account2)(dispatch, getState).then(() =>
expect(authentication.logout, 'to have a call satisfying', [
account2
])
)
);
});
});
describe('#logoutAll()', () => {
const account2 = {...account, id: 2};
beforeEach(() => {
getState.returns({ getState.returns({
accounts: { accounts: {
active: account2, active: account2,
available: [account] available: [account, account2]
}, },
user user
}); });
return revoke(account2)(dispatch, getState).then(() => { sinon.stub(authentication, 'logout').named('authentication.logout');
expect(dispatch, 'to have a call satisfying', [
remove(account2)
]);
expect(dispatch, 'to have a call satisfying', [
activate(account)
]);
expect(dispatch, 'to have a call satisfying', [
updateUser({...user, isGuest: false})
]);
// expect(dispatch, 'to have calls satisfying', [
// [remove(account2)],
// [expect.it('to be a function')]
// // [authenticate(account2)] // TODO: this is not a plain action. How should we simplify its testing?
// ])
});
}); });
it('should logout if no other accounts available', () => { afterEach(() => {
getState.returns({ authentication.logout.restore();
accounts: { });
active: account,
available: []
},
user
});
revoke(account)(dispatch, getState).then(() => { it('should call logout api method for each account', () => {
expect(dispatch, 'to have a call satisfying', [ logoutAll()(dispatch, getState);
{payload: {isGuest: true}}
// updateUser({isGuest: true}) expect(authentication.logout, 'to have calls satisfying', [
]); [account],
expect(dispatch, 'to have a call satisfying', [ [account2]
reset() ]);
]); });
// expect(dispatch, 'to have calls satisfying', [
// [remove(account)], it('should dispatch reset', () => {
// [expect.it('to be a function')] logoutAll()(dispatch, getState);
// // [logout()] // TODO: this is not a plain action. How should we simplify its testing?
// ]) expect(dispatch, 'to have a call satisfying', [
}); reset()
]);
}); });
}); });
}); });

View File

@ -42,11 +42,16 @@ describe('components/user/actions', () => {
}); });
describe('user with jwt', () => { describe('user with jwt', () => {
const token = 'iLoveRockNRoll';
beforeEach(() => { beforeEach(() => {
getState.returns({ getState.returns({
user: { user: {
token: 'iLoveRockNRoll',
lang: 'foo' lang: 'foo'
},
accounts: {
active: {token},
available: [{token}]
} }
}); });
}); });
@ -65,7 +70,7 @@ describe('components/user/actions', () => {
return callThunk(logout).then(() => { return callThunk(logout).then(() => {
expect(request.post, 'to have a call satisfying', [ expect(request.post, 'to have a call satisfying', [
'/api/authentication/logout' '/api/authentication/logout', {}, {}
]); ]);
}); });
}); });
@ -75,11 +80,17 @@ describe('components/user/actions', () => {
testRedirectedToLogin(); testRedirectedToLogin();
}); });
describe('user without jwt', () => { // (a guest with partially filled user's state) describe('user without jwt', () => {
// (a guest with partially filled user's state)
// DEPRECATED
beforeEach(() => { beforeEach(() => {
getState.returns({ getState.returns({
user: { user: {
lang: 'foo' lang: 'foo'
},
accounts: {
active: null,
available: []
} }
}); });
}); });

View File

@ -17,6 +17,7 @@ describe('refreshTokenMiddleware', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(authentication, 'requestToken').named('authentication.requestToken'); sinon.stub(authentication, 'requestToken').named('authentication.requestToken');
sinon.stub(authentication, 'logout').named('authentication.logout');
getState = sinon.stub().named('store.getState'); getState = sinon.stub().named('store.getState');
dispatch = sinon.spy((arg) => dispatch = sinon.spy((arg) =>
@ -28,6 +29,7 @@ describe('refreshTokenMiddleware', () => {
afterEach(() => { afterEach(() => {
authentication.requestToken.restore(); authentication.requestToken.restore();
authentication.logout.restore();
}); });
it('must be till 2100 to test with validToken', () => it('must be till 2100 to test with validToken', () =>
@ -37,12 +39,14 @@ describe('refreshTokenMiddleware', () => {
describe('#before', () => { describe('#before', () => {
describe('when token expired', () => { describe('when token expired', () => {
beforeEach(() => { beforeEach(() => {
const account = {
token: expiredToken,
refreshToken
};
getState.returns({ getState.returns({
accounts: { accounts: {
active: { active: account,
token: expiredToken, available: [account]
refreshToken
}
}, },
user: {} user: {}
}); });
@ -104,12 +108,14 @@ describe('refreshTokenMiddleware', () => {
}); });
it('should if token can not be parsed', () => { it('should if token can not be parsed', () => {
const account = {
token: 'realy bad token',
refreshToken
};
getState.returns({ getState.returns({
accounts: { accounts: {
active: { active: account,
token: 'realy bad token', available: [account]
refreshToken
}
}, },
user: {} user: {}
}); });
@ -140,7 +146,8 @@ describe('refreshTokenMiddleware', () => {
beforeEach(() => { beforeEach(() => {
getState.returns({ getState.returns({
accounts: { accounts: {
active: null active: null,
available: []
}, },
user: { user: {
token: expiredToken, token: expiredToken,
@ -216,7 +223,8 @@ describe('refreshTokenMiddleware', () => {
beforeEach(() => { beforeEach(() => {
getState.returns({ getState.returns({
accounts: { accounts: {
active: {refreshToken} active: {refreshToken},
available: [{refreshToken}]
}, },
user: {} user: {}
}); });

View File

@ -1,5 +1,6 @@
import expect from 'unexpected'; import expect from 'unexpected';
import request from 'services/request';
import authentication from 'services/api/authentication'; import authentication from 'services/api/authentication';
import accounts from 'services/api/accounts'; import accounts from 'services/api/accounts';
@ -88,4 +89,38 @@ describe('authentication api', () => {
}); });
}); });
}); });
describe('#logout', () => {
beforeEach(() => {
sinon.stub(request, 'post').named('request.post');
});
afterEach(() => {
request.post.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.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}
]);
});
});
}); });