From 9f926e42bc1278af51ddff96219c0786ec11cbe1 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Wed, 28 Feb 2018 23:58:02 +0200 Subject: [PATCH] #389: automatically revoke account, when user clicks back during re-login --- src/components/accounts/actions.js | 2 +- src/components/accounts/reducer.js | 4 ++ src/components/auth/actions.js | 7 ++- src/services/authFlow/AuthFlow.js | 2 + src/services/authFlow/PasswordState.js | 19 ++++++- src/services/authFlow/PasswordState.test.js | 32 +++++++++++ src/services/authFlow/index.js | 2 + tests-e2e/cypress/fixtures/example.json | 5 -- .../integration/invalid-refreshToken.test.js | 53 ++++++++++++++----- 9 files changed, 103 insertions(+), 23 deletions(-) delete mode 100644 tests-e2e/cypress/fixtures/example.json diff --git a/src/components/accounts/actions.js b/src/components/accounts/actions.js index cd7195a..9753cb4 100644 --- a/src/components/accounts/actions.js +++ b/src/components/accounts/actions.js @@ -29,7 +29,7 @@ type State = { }, }; -export { updateToken }; +export { updateToken, activate }; /** * @param {Account|object} account diff --git a/src/components/accounts/reducer.js b/src/components/accounts/reducer.js index 7cfe908..4dc0cc4 100644 --- a/src/components/accounts/reducer.js +++ b/src/components/accounts/reducer.js @@ -34,6 +34,10 @@ export function getActiveAccount(state: { accounts: State }): ?Account { return state.accounts.available.find((account) => account.id === accountId); } +export function getAvailableAccounts(state: { accounts: State }): Array { + return state.accounts.available; +} + export default function accounts( state: State = { active: null, diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js index edbb4ad..8e030c4 100644 --- a/src/components/auth/actions.js +++ b/src/components/auth/actions.js @@ -15,7 +15,12 @@ import { create as createPopup } from 'components/ui/popup/actions'; import ContactForm from 'components/contact/ContactForm'; export { updateUser } from 'components/user/actions'; -export { authenticate, logoutAll as logout } from 'components/accounts/actions'; +export { + authenticate, + logoutAll as logout, + revoke as removeAccount, + activate as activateAccount +} from 'components/accounts/actions'; import { getCredentials } from './reducer'; /** diff --git a/src/services/authFlow/AuthFlow.js b/src/services/authFlow/AuthFlow.js index 0d52d4e..b7bcdb8 100644 --- a/src/services/authFlow/AuthFlow.js +++ b/src/services/authFlow/AuthFlow.js @@ -25,6 +25,8 @@ type Request = { type ActionId = | 'updateUser' | 'authenticate' + | 'activateAccount' + | 'removeAccount' | 'logout' | 'goBack' | 'redirect' diff --git a/src/services/authFlow/PasswordState.js b/src/services/authFlow/PasswordState.js index a598882..6129102 100644 --- a/src/services/authFlow/PasswordState.js +++ b/src/services/authFlow/PasswordState.js @@ -1,6 +1,7 @@ // @flow import logger from 'services/logger'; import { getCredentials } from 'components/auth/reducer'; +import { getActiveAccount, getAvailableAccounts } from 'components/accounts/reducer'; import AbstractState from './AbstractState'; import ChooseAccountState from './ChooseAccountState'; @@ -63,10 +64,24 @@ export default class PasswordState extends AbstractState { } goBack(context: AuthContext) { - const { isRelogin } = getCredentials(context.getState()); + const state = context.getState(); + const { isRelogin } = getCredentials(state); if (isRelogin) { - context.setState(new ChooseAccountState()); + const availableAccounts = getAvailableAccounts(state); + const accountToRemove = getActiveAccount(state); + + if (availableAccounts.length === 1 || !accountToRemove) { + context.run('logout'); + context.run('setLogin', null); + context.setState(new LoginState()); + } else { + const accountToReplace = availableAccounts.find(({id}) => id !== accountToRemove.id); + + context.run('activateAccount', accountToReplace); + context.run('removeAccount', accountToRemove); + context.setState(new ChooseAccountState()); + } } else { context.run('setLogin', null); context.setState(new LoginState()); diff --git a/src/services/authFlow/PasswordState.test.js b/src/services/authFlow/PasswordState.test.js index 30a3aaf..0092a9e 100644 --- a/src/services/authFlow/PasswordState.test.js +++ b/src/services/authFlow/PasswordState.test.js @@ -173,6 +173,13 @@ describe('PasswordState', () => { it('should transition to ChooseAccountState if this is relogin', () => { context.getState.returns({ + accounts: { + active: 1, + available: [ + {id: 1}, + {id: 2} + ] + }, auth: { credentials: { login: 'foo', @@ -181,9 +188,34 @@ describe('PasswordState', () => { } }); + expectRun(mock, 'activateAccount', { id: 2 }); + expectRun(mock, 'removeAccount', { id: 1 }); expectState(mock, ChooseAccountState); state.goBack(context); }); + + it('should transition to LoginState if this is relogin and only one account available', () => { + context.getState.returns({ + accounts: { + active: 1, + available: [ + {id: 1}, + ] + }, + auth: { + credentials: { + login: 'foo', + isRelogin: true, + } + } + }); + + expectRun(mock, 'logout'); + expectRun(mock, 'setLogin', null); + expectState(mock, LoginState); + + state.goBack(context); + }); }); }); diff --git a/src/services/authFlow/index.js b/src/services/authFlow/index.js index 4cf624c..01097d9 100644 --- a/src/services/authFlow/index.js +++ b/src/services/authFlow/index.js @@ -7,6 +7,8 @@ import * as actions from 'components/auth/actions'; const availableActions = { updateUser: actions.updateUser, authenticate: actions.authenticate, + activateAccount: actions.activateAccount, + removeAccount: actions.removeAccount, logout: actions.logout, goBack: actions.goBack, redirect: actions.redirect, diff --git a/tests-e2e/cypress/fixtures/example.json b/tests-e2e/cypress/fixtures/example.json deleted file mode 100644 index da18d93..0000000 --- a/tests-e2e/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/tests-e2e/cypress/integration/invalid-refreshToken.test.js b/tests-e2e/cypress/integration/invalid-refreshToken.test.js index a1fe29e..f2e278f 100644 --- a/tests-e2e/cypress/integration/invalid-refreshToken.test.js +++ b/tests-e2e/cypress/integration/invalid-refreshToken.test.js @@ -1,15 +1,15 @@ import { account1, account2 } from '../fixtures/accounts.json'; +const multiAccount + = '{"accounts":{"available":[{"id":7,"username":"SleepWalker","email":"danilenkos@auroraglobal.com","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4MDksImV4cCI6MTUxODM3NzQwOSwic3ViIjoiZWx5fDciLCJqdGkiOjM1NDh9.Fv4AbJ0iDbrH3bhbgF0ViJLfYYiwH78deR4fMlMhKrQ","refreshToken":"3gh6ZZ3R9jGeFdp0TmlY7sd0zBxH6Zfq48M86eUAv952RcAKx32RAnjlKkgd6i-MV-RKbjtADIdoRwMUWOYQjEYtwwXPjcQJ"},{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}'; +const singleAccount + = '{"accounts":{"available":[{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}'; + describe('when user\'s token and refreshToken are invalid', () => { beforeEach(() => cy .visit('/') - .then(() => - localStorage.setItem( - 'redux-storage', - '{"accounts":{"available":[{"id":7,"username":"SleepWalker","email":"danilenkos@auroraglobal.com","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4MDksImV4cCI6MTUxODM3NzQwOSwic3ViIjoiZWx5fDciLCJqdGkiOjM1NDh9.Fv4AbJ0iDbrH3bhbgF0ViJLfYYiwH78deR4fMlMhKrQ","refreshToken":"3gh6ZZ3R9jGeFdp0TmlY7sd0zBxH6Zfq48M86eUAv952RcAKx32RAnjlKkgd6i-MV-RKbjtADIdoRwMUWOYQjEYtwwXPjcQJ"},{"id":102,"username":"test","email":"admin@udf.su","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1MTgzNzM4NjUsImV4cCI6MTUxODM3NzQ2NSwic3ViIjoiZWx5fDEwMiIsImp0aSI6MzU0OX0.eJEgvXT3leGqBe3tYNGZb0E4WEvWfrLPjcD7eNjyQYO","refreshToken":"Al75SIx-LFOCP7kaqZBVqMVmSljJw9_bdFQGyuM64c6ShP7YsXbkCD8vPOundAwUDfRZqsIbOHUROmAHPB0VBfjLfw96yqxx"}],"active":102},"user":{"id":102,"uuid":"e49cafdc-6e0c-442d-b608-dacdb864ee34","username":"test","token":"","email":"admin@udf.su","maskedEmail":"","avatar":"","lang":"en","isActive":true,"isOtpEnabled":true,"shouldAcceptRules":false,"passwordChangedAt":1478961317,"hasMojangUsernameCollision":true,"isGuest":false,"registeredAt":1478961317,"elyProfileLink":"http://ely.by/u102","originalResponse":{}}}' - ) - ) + .then(() => localStorage.setItem('redux-storage', multiAccount)) ); it('should ask for password', () => { @@ -31,6 +31,11 @@ describe('when user\'s token and refreshToken are invalid', () => { cy.url().should('include', '/choose-account'); + cy + .get('[data-e2e-content]') + .contains(account2.email) + .should('not.exist'); + cy .get('[data-e2e-content]') .contains(account1.username) @@ -40,14 +45,36 @@ describe('when user\'s token and refreshToken are invalid', () => { cy.contains('account preferences'); }); + it('it should redirect to login, when one account and clicking back', () => { + cy + .url() + .should(() => localStorage.setItem('redux-storage', singleAccount)); + cy.visit('/'); + + cy.get('[data-e2e-go-back]').click(); + + cy.url().should('include', '/login'); + + cy.get('[data-e2e-toolbar]').contains('Join'); + }); + 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 + .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'); + cy + .get('[data-e2e-toolbar]') + .contains(account2.username) + .should('not.exist'); }); it('should allow enter new login from choose account', () => { @@ -98,12 +125,12 @@ describe('when user\'s token and refreshToken are invalid', () => { cy .get('[data-e2e-content]') - .contains(account2.username) + .contains(account1.username) .click(); cy.url().should('include', '/password'); - cy.get('[name="password"]').type(`${account2.password}{enter}`); + cy.get('[name="password"]').type(`${account1.password}{enter}`); cy.location('pathname', { timeout: 15000 }).should('eq', '/'); cy.contains('account preferences'); @@ -133,9 +160,7 @@ describe('when user\'s token and refreshToken are invalid', () => { cy.contains('[type=submit]', 'Log into another account').click(); cy.contains('a', 'Create new account').click(); - cy - .get('@fetch') - .should('be.calledWith', '/api/options'); + cy.get('@fetch').should('be.calledWith', '/api/options'); cy.url().should('contain', '/register'); });