From 586cdfffe4b3e45024bd90076c42fa06e55ea018 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sat, 12 Nov 2016 22:31:44 +0200 Subject: [PATCH] #48: add/remove accounts from account switcher. Allow authorized users to log in into another account --- src/components/accounts/AccountSwitcher.jsx | 58 +++++++++++------ src/components/accounts/actions.js | 7 ++- src/components/accounts/reducer.js | 6 +- src/components/auth/PanelTransition.jsx | 28 +++++++-- src/components/auth/actions.js | 30 ++++----- src/components/auth/reducer.js | 30 ++++++++- src/components/userbar/LoggedInPanel.jsx | 10 ++- src/services/authFlow/LoginState.js | 10 ++- src/services/authFlow/PasswordState.js | 10 +-- tests/components/accounts/actions.test.js | 27 ++++---- tests/components/accounts/reducer.test.js | 22 +++++-- tests/components/auth/actions.test.js | 44 +++++++++---- tests/components/auth/reducer.test.js | 16 +++++ .../refreshTokenMiddleware.test.js | 2 +- .../authFlow/AuthFlow.functional.test.js | 3 + tests/services/authFlow/LoginState.test.js | 17 ++--- tests/services/authFlow/PasswordState.test.js | 63 ++++++++----------- 17 files changed, 243 insertions(+), 140 deletions(-) create mode 100644 tests/components/auth/reducer.test.js diff --git a/src/components/accounts/AccountSwitcher.jsx b/src/components/accounts/AccountSwitcher.jsx index ea087aa..428886f 100644 --- a/src/components/accounts/AccountSwitcher.jsx +++ b/src/components/accounts/AccountSwitcher.jsx @@ -10,20 +10,13 @@ import { Button } from 'components/ui/form'; import styles from './accountSwitcher.scss'; import messages from './AccountSwitcher.intl.json'; -const accounts = { - active: {id: 7, username: 'SleepWalker', email: 'danilenkos@auroraglobal.com'}, - available: [ - {id: 7, username: 'SleepWalker', email: 'danilenkos@auroraglobal.com'}, - {id: 8, username: 'ErickSkrauch', email: 'erickskrauch@yandex.ru'}, - {id: 9, username: 'Ely-en', email: 'ely-en@ely.by'}, - {id: 10, username: 'Ely-by', email: 'ely-pt@ely.by'}, - ] -}; - -export default class AccountSwitcher extends Component { +export class AccountSwitcher extends Component { static displayName = 'AccountSwitcher'; static propTypes = { + switchAccount: PropTypes.func.isRequired, + removeAccount: PropTypes.func.isRequired, + onAfterAction: PropTypes.func, // called after each action performed accounts: PropTypes.shape({ // TODO: accounts shape active: PropTypes.shape({ id: PropTypes.number @@ -43,7 +36,7 @@ export default class AccountSwitcher extends Component { highlightActiveAccount: true, allowLogout: true, allowAdd: true, - accounts + onAfterAction() {} }; render() { @@ -66,7 +59,7 @@ export default class AccountSwitcher extends Component { styles.accountIcon, styles.activeAccountIcon, styles.accountIcon1 - )}> + )} />
{accounts.active.username} @@ -81,7 +74,7 @@ export default class AccountSwitcher extends Component {
- +
@@ -90,16 +83,19 @@ export default class AccountSwitcher extends Component {
) : null} {available.map((account, id) => ( -
+
+ )} /> {allowLogout ? ( -
+
) : ( -
+
)}
@@ -113,7 +109,7 @@ export default class AccountSwitcher extends Component {
))} {allowAdd ? ( - +
- +
); } - toggleAccountSwitcher() { - this.setState({ - isAccountSwitcherActive: !this.state.isAccountSwitcherActive - }); - } + toggleAccountSwitcher = () => this.setState({ + isAccountSwitcherActive: !this.state.isAccountSwitcherActive + }); onExpandAccountSwitcher = (event) => { event.preventDefault(); diff --git a/src/services/authFlow/LoginState.js b/src/services/authFlow/LoginState.js index 5ee0fb0..bbb5c5a 100644 --- a/src/services/authFlow/LoginState.js +++ b/src/services/authFlow/LoginState.js @@ -3,11 +3,15 @@ import PasswordState from './PasswordState'; export default class LoginState extends AbstractState { enter(context) { - const {user} = context.getState(); + const {auth, user} = context.getState(); - if (user.email || user.username) { + // TODO: it may not allow user to leave password state till he click back or enters password + if (auth.login) { context.setState(new PasswordState()); - } else { + } else if (user.isGuest + // for the case, when user is logged in and wants to add a new aacount + || /login|password/.test(context.getRequest().path) // TODO: improve me + ) { context.navigate('/login'); } } diff --git a/src/services/authFlow/PasswordState.js b/src/services/authFlow/PasswordState.js index 13e6c22..039f54b 100644 --- a/src/services/authFlow/PasswordState.js +++ b/src/services/authFlow/PasswordState.js @@ -5,9 +5,9 @@ import LoginState from './LoginState'; export default class PasswordState extends AbstractState { enter(context) { - const {user} = context.getState(); + const {auth} = context.getState(); - if (user.isGuest) { + if (auth.login) { context.navigate('/password'); } else { context.setState(new CompleteState()); @@ -15,12 +15,12 @@ export default class PasswordState extends AbstractState { } resolve(context, {password, rememberMe}) { - const {user} = context.getState(); + const {auth: {login}} = context.getState(); context.run('login', { password, rememberMe, - login: user.email || user.username + login }) .then(() => context.setState(new CompleteState())); } @@ -30,7 +30,7 @@ export default class PasswordState extends AbstractState { } goBack(context) { - context.run('logout'); + context.run('setLogin', null); context.setState(new LoginState()); } } diff --git a/tests/components/accounts/actions.test.js b/tests/components/accounts/actions.test.js index 94d363f..fffab50 100644 --- a/tests/components/accounts/actions.test.js +++ b/tests/components/accounts/actions.test.js @@ -22,7 +22,7 @@ const user = { lang: 'be' }; -describe('Accounts actions', () => { +describe('components/accounts/actions', () => { let dispatch; let getState; @@ -109,19 +109,15 @@ describe('Accounts actions', () => { }); describe('#revoke()', () => { - it(`should dispatch ${REMOVE} action`, () => { - revoke(account)(dispatch, getState); - - expect(dispatch, 'to have a call satisfying', [ - remove(account) - ]); - }); - it('should switch next account if available', () => { const account2 = {...account, id: 2}; getState.returns({ - accounts: [account] + accounts: { + active: account2, + available: [account] + }, + user }); return revoke(account2)(dispatch, getState).then(() => { @@ -143,10 +139,15 @@ describe('Accounts actions', () => { }); it('should logout if no other accounts available', () => { + getState.returns({ + accounts: { + active: account, + available: [] + }, + user + }); + revoke(account)(dispatch, getState).then(() => { - expect(dispatch, 'to have a call satisfying', [ - remove(account) - ]); expect(dispatch, 'to have a call satisfying', [ {payload: {isGuest: true}} // updateUser({isGuest: true}) diff --git a/tests/components/accounts/reducer.test.js b/tests/components/accounts/reducer.test.js index 58ef367..3769870 100644 --- a/tests/components/accounts/reducer.test.js +++ b/tests/components/accounts/reducer.test.js @@ -45,11 +45,23 @@ describe('Accounts reducer', () => { }) ); - it('should not add the same account twice', () => - expect(accounts({...initial, available: [account]}, add(account)), 'to satisfy', { - available: [account] - }) - ); + it('should replace if account was added for the second time', () => { + const outdatedAccount = { + ...account, + someShit: true + }; + + const updatedAccount = { + ...account, + token: 'newToken' + }; + + return expect( + accounts({...initial, available: [outdatedAccount]}, add(updatedAccount)), + 'to satisfy', { + available: [updatedAccount] + }); + }); it('throws, when account is invalid', () => { expect(() => accounts(initial, add()), diff --git a/tests/components/auth/actions.test.js b/tests/components/auth/actions.test.js index 4c7ff9f..8afd00b 100644 --- a/tests/components/auth/actions.test.js +++ b/tests/components/auth/actions.test.js @@ -10,7 +10,9 @@ import { setOAuthRequest, setScopes, setOAuthCode, - requirePermissionsAccept + requirePermissionsAccept, + login, + setLogin } from 'components/auth/actions'; const oauthData = { @@ -22,8 +24,8 @@ const oauthData = { }; describe('components/auth/actions', () => { - const dispatch = sinon.stub().named('dispatch'); - const getState = sinon.stub().named('getState'); + const dispatch = sinon.stub().named('store.dispatch'); + const getState = sinon.stub().named('store.getState'); function callThunk(fn, ...args) { const thunk = fn(...args); @@ -67,21 +69,21 @@ describe('components/auth/actions', () => { request.get.returns(Promise.resolve(resp)); }); - it('should send get request to an api', () => { - return callThunk(oAuthValidate, oauthData).then(() => { + it('should send get request to an api', () => + callThunk(oAuthValidate, oauthData).then(() => { expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]); - }); - }); + }) + ); - it('should dispatch setClient, setOAuthRequest and setScopes', () => { - return callThunk(oAuthValidate, oauthData).then(() => { + it('should dispatch setClient, setOAuthRequest and setScopes', () => + callThunk(oAuthValidate, oauthData).then(() => { expectDispatchCalls([ [setClient(resp.client)], [setOAuthRequest(resp.oAuth)], [setScopes(resp.session.scopes)] ]); - }); - }); + }) + ); }); describe('#oAuthComplete()', () => { @@ -160,4 +162,24 @@ describe('components/auth/actions', () => { }); }); }); + + describe('#login()', () => { + describe('when correct login was entered', () => { + beforeEach(() => { + request.post.returns(Promise.reject({ + errors: { + password: 'error.password_required' + } + })); + }); + + it('should set login', () => + callThunk(login, {login: 'foo'}).then(() => { + expectDispatchCalls([ + [setLogin('foo')] + ]); + }) + ); + }); + }); }); diff --git a/tests/components/auth/reducer.test.js b/tests/components/auth/reducer.test.js new file mode 100644 index 0000000..66024a9 --- /dev/null +++ b/tests/components/auth/reducer.test.js @@ -0,0 +1,16 @@ +import expect from 'unexpected'; + +import auth from 'components/auth/reducer'; +import { setLogin, SET_LOGIN } from 'components/auth/actions'; + +describe('auth reducer', () => { + describe(SET_LOGIN, () => { + it('should set login', () => { + const expectedLogin = 'foo'; + + expect(auth(undefined, setLogin(expectedLogin)), 'to satisfy', { + login: expectedLogin + }); + }); + }); +}); diff --git a/tests/components/user/middlewares/refreshTokenMiddleware.test.js b/tests/components/user/middlewares/refreshTokenMiddleware.test.js index 82b1114..6068872 100644 --- a/tests/components/user/middlewares/refreshTokenMiddleware.test.js +++ b/tests/components/user/middlewares/refreshTokenMiddleware.test.js @@ -68,7 +68,7 @@ describe('refreshTokenMiddleware', () => { }); it('should not apply to refresh-token request', () => { - const data = {url: '/refresh-token'}; + const data = {url: '/refresh-token', options: {}}; const resp = middleware.before(data); expect(resp, 'to satisfy', data); diff --git a/tests/services/authFlow/AuthFlow.functional.test.js b/tests/services/authFlow/AuthFlow.functional.test.js index 9d135d5..fa281dc 100644 --- a/tests/services/authFlow/AuthFlow.functional.test.js +++ b/tests/services/authFlow/AuthFlow.functional.test.js @@ -47,6 +47,9 @@ describe('AuthFlow.functional', () => { state.user = { isGuest: true }; + state.auth = { + login: null + }; }); it('should redirect guest / -> /login', () => { diff --git a/tests/services/authFlow/LoginState.test.js b/tests/services/authFlow/LoginState.test.js index 60ad0b3..54602a5 100644 --- a/tests/services/authFlow/LoginState.test.js +++ b/tests/services/authFlow/LoginState.test.js @@ -1,6 +1,5 @@ import LoginState from 'services/authFlow/LoginState'; import PasswordState from 'services/authFlow/PasswordState'; -import ForgotPasswordState from 'services/authFlow/ForgotPasswordState'; import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; @@ -24,7 +23,8 @@ describe('LoginState', () => { describe('#enter', () => { it('should navigate to /login', () => { context.getState.returns({ - user: {isGuest: true} + user: {isGuest: true}, + auth: {login: null} }); expectNavigate(mock, '/login'); @@ -32,22 +32,15 @@ describe('LoginState', () => { state.enter(context); }); - const testTransitionToPassword = (user) => { + it('should transition to password if login was set', () => { context.getState.returns({ - user: user + user: {isGuest: true}, + auth: {login: 'foo'} }); expectState(mock, PasswordState); state.enter(context); - }; - - it('should transition to password if has email', () => { - testTransitionToPassword({email: 'foo'}); - }); - - it('should transition to password if has username', () => { - testTransitionToPassword({username: 'foo'}); }); }); diff --git a/tests/services/authFlow/PasswordState.test.js b/tests/services/authFlow/PasswordState.test.js index 5a003de..b92578a 100644 --- a/tests/services/authFlow/PasswordState.test.js +++ b/tests/services/authFlow/PasswordState.test.js @@ -25,7 +25,8 @@ describe('PasswordState', () => { describe('#enter', () => { it('should navigate to /password', () => { context.getState.returns({ - user: {isGuest: true} + user: {isGuest: true}, + auth: {login: 'foo'} }); expectNavigate(mock, '/password'); @@ -35,7 +36,8 @@ describe('PasswordState', () => { it('should transition to complete if not guest', () => { context.getState.returns({ - user: {isGuest: false} + user: {isGuest: false}, + auth: {login: null} }); expectState(mock, CompleteState); @@ -45,42 +47,29 @@ describe('PasswordState', () => { }); describe('#resolve', () => { - (function() { - const expectedLogin = 'login'; - const expectedPassword = 'password'; + it('should call login with login and password', () => { + const expectedLogin = 'foo'; + const expectedPassword = 'bar'; const expectedRememberMe = true; - const testWith = (user) => { - it(`should call login with email or username and password. User: ${JSON.stringify(user)}`, () => { - context.getState.returns({user}); - - expectRun( - mock, - 'login', - sinon.match({ - login: expectedLogin, - password: expectedPassword, - rememberMe: expectedRememberMe, - }) - ).returns({then() {}}); - - state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe}); - }); - }; - - testWith({ - email: expectedLogin + context.getState.returns({ + auth: { + login: expectedLogin + } }); - testWith({ - username: expectedLogin - }); + expectRun( + mock, + 'login', + sinon.match({ + login: expectedLogin, + password: expectedPassword, + rememberMe: expectedRememberMe, + }) + ).returns({then() {}}); - testWith({ - email: expectedLogin, - username: expectedLogin - }); - }()); + state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe}); + }); it('should transition to complete state on successfull login', () => { const promise = Promise.resolve(); @@ -88,8 +77,8 @@ describe('PasswordState', () => { const expectedPassword = 'password'; context.getState.returns({ - user: { - email: expectedLogin + auth: { + login: expectedLogin } }); @@ -111,8 +100,8 @@ describe('PasswordState', () => { }); describe('#goBack', () => { - it('should transition to forgot password state', () => { - expectRun(mock, 'logout'); + it('should transition to login state', () => { + expectRun(mock, 'setLogin', null); expectState(mock, LoginState); state.goBack(context);