From f1d33bf7ecbd5a82c5db86ace0e61b179d978e5d Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Tue, 27 Feb 2018 23:17:31 +0200 Subject: [PATCH] #389: fix logout in case, when all the accounts have invalid tokens --- src/components/accounts/AccountSwitcher.js | 20 +++++-------- src/components/accounts/actions.js | 30 +++++++++++++++---- src/components/accounts/actions.test.js | 29 ++++++++++++++++++ src/components/userbar/LoggedInPanel.js | 10 ++----- src/components/userbar/Userbar.js | 25 +++++++--------- src/pages/root/RootPage.js | 13 ++++---- tests-e2e/cypress/fixtures/accounts.json | 2 ++ .../integration/invalid-refreshToken.test.js | 11 +++++++ 8 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/components/accounts/AccountSwitcher.js b/src/components/accounts/AccountSwitcher.js index 69ec944..c42093d 100644 --- a/src/components/accounts/AccountSwitcher.js +++ b/src/components/accounts/AccountSwitcher.js @@ -1,14 +1,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; - +import { connect } from 'react-redux'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; import { FormattedMessage as Message } from 'react-intl'; - import loader from 'services/loader'; import { skins, SKIN_DARK, COLOR_WHITE } from 'components/ui'; import { Button } from 'components/ui/form'; -import { userShape } from 'components/user/User'; +import { authenticate, revoke } from 'components/accounts/actions'; +import { getActiveAccount } from 'components/accounts/reducer'; import styles from './accountSwitcher.scss'; import messages from './AccountSwitcher.intl.json'; @@ -22,7 +22,6 @@ export class AccountSwitcher extends Component { onAfterAction: PropTypes.func, // called after each action performed onSwitch: PropTypes.func, // called after switching an account. The active account will be passed as arg accounts: PropTypes.object, // eslint-disable-line - user: userShape, // TODO: remove me, when we will be sure, that accounts.active is always set for user (event after register) skin: PropTypes.oneOf(skins), highlightActiveAccount: PropTypes.bool, // whether active account should be expanded and shown on the top allowLogout: PropTypes.bool, // whether to show logout icon near each account @@ -40,8 +39,7 @@ export class AccountSwitcher extends Component { render() { const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props; - // const activeAccount = accounts.active || this.props.user; - const activeAccount = this.props.user; + const activeAccount = getActiveAccount({ accounts }); let {available} = accounts; @@ -83,14 +81,14 @@ export class AccountSwitcher extends Component { ) : null} - {available.map((account, id) => ( + {available.map((account, index) => (
{allowLogout ? ( @@ -156,12 +154,8 @@ export class AccountSwitcher extends Component { }; } -import { connect } from 'react-redux'; -import { authenticate, revoke } from 'components/accounts/actions'; - -export default connect(({accounts, user}) => ({ +export default connect(({accounts}) => ({ accounts, - user }), { switchAccount: authenticate, removeAccount: revoke diff --git a/src/components/accounts/actions.js b/src/components/accounts/actions.js index f646c62..cd7195a 100644 --- a/src/components/accounts/actions.js +++ b/src/components/accounts/actions.js @@ -9,6 +9,7 @@ import { setAccountSwitcher } from 'components/auth/actions'; import { getActiveAccount } from 'components/accounts/reducer'; import logger from 'services/logger'; +import type { Account, State as AccountsState } from './reducer'; import { add, remove, @@ -16,7 +17,6 @@ import { reset, updateToken } from './actions/pure-actions'; -import type { Account, State as AccountsState } from './reducer'; type Dispatch = (action: Object) => Promise<*>; @@ -45,8 +45,19 @@ export function authenticate(account: Account | { const {token, refreshToken} = account; const email = account.email || null; - return (dispatch: Dispatch, getState: () => State): Promise => - authentication.validateToken({token, refreshToken}) + return (dispatch: Dispatch, getState: () => State): Promise => { + const accountId: number | null = typeof account.id === 'number' ? account.id : null; + const knownAccount: ?Account = accountId + ? getState().accounts.available.find((item) => item.id === accountId) + : null; + + if (knownAccount) { + // this account is already available + // activate it before validation + dispatch(activate(knownAccount)); + } + + return authentication.validateToken({token, refreshToken}) .catch((resp = {}) => { // all the logic to get the valid token was failed, // looks like we have some problems with token @@ -97,6 +108,7 @@ export function authenticate(account: Account | { return dispatch(setLocale(user.lang)) .then(() => account); }); + }; } /** @@ -219,9 +231,17 @@ export function revoke(account: Account) { if (accountToReplace) { return dispatch(authenticate(accountToReplace)) - .then(() => { - authentication.logout(account); + .finally(() => { + // we need to logout user, even in case, when we can + // not authenticate him with new account + authentication.logout(account) + .catch(() => { + // we don't care + }); dispatch(remove(account)); + }) + .catch(() => { + // we don't care }); } diff --git a/src/components/accounts/actions.test.js b/src/components/accounts/actions.test.js index 14bf959..68ec1b2 100644 --- a/src/components/accounts/actions.test.js +++ b/src/components/accounts/actions.test.js @@ -190,6 +190,35 @@ describe('components/accounts/actions', () => { ) ); }); + + describe('when one account available', () => { + beforeEach(() => { + getState.returns({ + accounts: { + active: account.id, + available: [account] + }, + auth: { + credentials: {}, + }, + user, + }); + }); + + it('should activate account before auth api call', () => { + authentication.validateToken.returns(Promise.reject({ error: 'foo'})); + + return expect( + authenticate(account)(dispatch, getState), + 'to be rejected with', + { error: 'foo'} + ).then(() => + expect(dispatch, 'to have a call satisfying', [ + activate(account) + ]) + ); + }); + }); }); describe('#revoke()', () => { diff --git a/src/components/userbar/LoggedInPanel.js b/src/components/userbar/LoggedInPanel.js index 5dffcd9..2590722 100644 --- a/src/components/userbar/LoggedInPanel.js +++ b/src/components/userbar/LoggedInPanel.js @@ -1,16 +1,12 @@ // @flow import React, { Component } from 'react'; - import classNames from 'classnames'; - import { AccountSwitcher } from 'components/accounts'; import styles from './loggedInPanel.scss'; -import type { User } from 'components/user'; - export default class LoggedInPanel extends Component<{ - user: User + username: string }, { isAccountSwitcherActive: bool }> { @@ -38,7 +34,7 @@ export default class LoggedInPanel extends Component<{ } render() { - const { user } = this.props; + const { username } = this.props; const { isAccountSwitcherActive } = this.state; return ( @@ -48,7 +44,7 @@ export default class LoggedInPanel extends Component<{ })}> diff --git a/src/components/userbar/Userbar.js b/src/components/userbar/Userbar.js index 713a460..b170f3e 100644 --- a/src/components/userbar/Userbar.js +++ b/src/components/userbar/Userbar.js @@ -1,31 +1,26 @@ -import PropTypes from 'prop-types'; +// @flow +import type { Account } from 'components/accounts/reducer'; import React, { Component } from 'react'; - import { Link } from 'react-router-dom'; import { FormattedMessage as Message } from 'react-intl'; - import buttons from 'components/ui/buttons.scss'; import messages from './Userbar.intl.json'; import styles from './userbar.scss'; - -import { userShape } from 'components/user/User'; - import LoggedInPanel from './LoggedInPanel'; -export default class Userbar extends Component { +export default class Userbar extends Component<{ + account: ?Account, + guestAction: 'register' | 'login', +}> { static displayName = 'Userbar'; - static propTypes = { - user: userShape, - guestAction: PropTypes.oneOf(['register', 'login']) - }; static defaultProps = { guestAction: 'register' }; render() { - const { user } = this.props; + const { account } = this.props; let { guestAction } = this.props; switch (guestAction) { @@ -48,12 +43,12 @@ export default class Userbar extends Component { return (
- {user.isGuest + {account ? ( - guestAction + ) : ( - + guestAction ) }
diff --git a/src/pages/root/RootPage.js b/src/pages/root/RootPage.js index 57b3914..6ac79c2 100644 --- a/src/pages/root/RootPage.js +++ b/src/pages/root/RootPage.js @@ -1,4 +1,6 @@ // @flow +import type { User } from 'components/user'; +import type { Account } from 'components/accounts/reducer'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { resetAuth } from 'components/auth/actions'; @@ -7,24 +9,23 @@ import { FormattedMessage as Message } from 'react-intl'; import { Route, Link, Switch } from 'react-router-dom'; import Helmet from 'react-helmet'; import classNames from 'classnames'; - import AuthPage from 'pages/auth/AuthPage'; import ProfilePage from 'pages/profile/ProfilePage'; import RulesPage from 'pages/rules/RulesPage'; import PageNotFound from 'pages/404/PageNotFound'; - import { ScrollIntoView } from 'components/ui/scroll'; import PrivateRoute from 'containers/PrivateRoute'; import AuthFlowRoute from 'containers/AuthFlowRoute'; import Userbar from 'components/userbar/Userbar'; import PopupStack from 'components/ui/popup/PopupStack'; import loader from 'services/loader'; -import type { User } from 'components/user'; +import { getActiveAccount } from 'components/accounts/reducer'; import styles from './root.scss'; import messages from './RootPage.intl.json'; class RootPage extends Component<{ + account: ?Account, user: User, isPopupActive: bool, onLogoClick: Function, @@ -46,7 +47,7 @@ class RootPage extends Component<{ render() { const props = this.props; - const {user, isPopupActive, onLogoClick} = this.props; + const {user, account, isPopupActive, onLogoClick} = this.props; const isRegisterPage = props.location.pathname === '/register'; if (document && document.body) { @@ -70,7 +71,8 @@ class RootPage extends Component<{
-
@@ -95,6 +97,7 @@ class RootPage extends Component<{ export default withRouter(connect((state) => ({ user: state.user, + account: getActiveAccount(state), isPopupActive: state.popup.popups.length > 0 }), { onLogoClick: resetAuth diff --git a/tests-e2e/cypress/fixtures/accounts.json b/tests-e2e/cypress/fixtures/accounts.json index b8c84d3..617811e 100644 --- a/tests-e2e/cypress/fixtures/accounts.json +++ b/tests-e2e/cypress/fixtures/accounts.json @@ -1,11 +1,13 @@ { "account1": { "username": "SleepWalker", + "email": "danilenkos@auroraglobal.com", "login": "SleepWalker", "password": "qwer1234" }, "account2": { "username": "test", + "email": "admin@udf.su", "login": "test", "password": "qwer1234" } diff --git a/tests-e2e/cypress/integration/invalid-refreshToken.test.js b/tests-e2e/cypress/integration/invalid-refreshToken.test.js index ed13abf..a1fe29e 100644 --- a/tests-e2e/cypress/integration/invalid-refreshToken.test.js +++ b/tests-e2e/cypress/integration/invalid-refreshToken.test.js @@ -24,6 +24,7 @@ describe('when user\'s token and refreshToken are invalid', () => { }); it('should allow select account', () => { + // TODO: need a way to get valid token for one of the accounts cy.visit('/'); cy.get('[data-e2e-go-back]').click(); @@ -39,6 +40,16 @@ describe('when user\'s token and refreshToken are invalid', () => { cy.contains('account preferences'); }); + it('should allow logout', () => { + cy.visit('/'); + + cy.get('[data-e2e-toolbar]').contains(account2.username).click(); + cy.get('[data-e2e-toolbar]').contains('Log out').click(); + + cy.contains(account2.email).should('not.exist'); + cy.get('[data-e2e-toolbar]').contains(account2.username).should('not.exist'); + }); + it('should allow enter new login from choose account', () => { cy.visit('/');