Merge branch '245-multiacc-improvement' into develop

Conflicts:
	frontend/src/index.js
This commit is contained in:
SleepWalker 2017-01-31 08:09:16 +02:00
commit 477b79918f
25 changed files with 570 additions and 206 deletions

View File

@ -3,8 +3,19 @@ import { routeActions } from 'react-router-redux';
import authentication from 'services/api/authentication';
import { updateUser, setGuest } from 'components/user/actions';
import { setLocale } from 'components/i18n/actions';
import { setAccountSwitcher } from 'components/auth/actions';
import logger from 'services/logger';
import {
add,
remove,
activate,
reset,
updateToken
} from 'components/accounts/actions/pure-actions';
export { updateToken };
/**
* @typedef {object} Account
* @property {string} id
@ -22,7 +33,7 @@ import logger from 'services/logger';
* @return {function}
*/
export function authenticate({token, refreshToken}) {
return (dispatch) =>
return (dispatch, getState) =>
authentication.validateToken({token, refreshToken})
.catch((resp) => {
logger.warn('Error validating token during auth', {
@ -46,6 +57,8 @@ export function authenticate({token, refreshToken}) {
}
}))
.then(({user, account}) => {
const {auth} = getState();
dispatch(add(account));
dispatch(activate(account));
dispatch(updateUser(user));
@ -58,12 +71,22 @@ export function authenticate({token, refreshToken}) {
sessionStorage.setItem(`stranger${account.id}`, 1);
}
if (auth && auth.oauth && auth.oauth.clientId) {
// if we authenticating during oauth, we disable account chooser
// because user probably has made his choise now
// this may happen, when user registers, logs in or uses account
// chooser panel during oauth
dispatch(setAccountSwitcher(false));
}
return dispatch(setLocale(user.lang))
.then(() => account);
});
}
/**
* Remove one account from current user's account list
*
* @param {Account} account
*
* @return {function}
@ -135,73 +158,3 @@ export function logoutStrangers() {
return Promise.resolve();
};
}
export const ADD = 'accounts:add';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function add(account) {
return {
type: ADD,
payload: account
};
}
export const REMOVE = 'accounts:remove';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function remove(account) {
return {
type: REMOVE,
payload: account
};
}
export const ACTIVATE = 'accounts:activate';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function activate(account) {
return {
type: ACTIVATE,
payload: account
};
}
export const RESET = 'accounts:reset';
/**
* @api private
*
* @return {object} - action definition
*/
export function reset() {
return {
type: RESET
};
}
export const UPDATE_TOKEN = 'accounts:updateToken';
/**
* @param {string} token
*
* @return {object} - action definition
*/
export function updateToken(token) {
return {
type: UPDATE_TOKEN,
payload: token
};
}

View File

@ -0,0 +1,69 @@
export const ADD = 'accounts:add';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function add(account) {
return {
type: ADD,
payload: account
};
}
export const REMOVE = 'accounts:remove';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function remove(account) {
return {
type: REMOVE,
payload: account
};
}
export const ACTIVATE = 'accounts:activate';
/**
* @api private
*
* @param {Account} account
*
* @return {object} - action definition
*/
export function activate(account) {
return {
type: ACTIVATE,
payload: account
};
}
export const RESET = 'accounts:reset';
/**
* @api private
*
* @return {object} - action definition
*/
export function reset() {
return {
type: RESET
};
}
export const UPDATE_TOKEN = 'accounts:updateToken';
/**
* @param {string} token
*
* @return {object} - action definition
*/
export function updateToken(token) {
return {
type: UPDATE_TOKEN,
payload: token
};
}

View File

@ -1,4 +1,4 @@
import { ADD, REMOVE, ACTIVATE, RESET, UPDATE_TOKEN } from './actions';
import { ADD, REMOVE, ACTIVATE, RESET, UPDATE_TOKEN } from './actions/pure-actions';
/**
* @typedef {AccountsState}

View File

@ -67,6 +67,9 @@ class PanelTransition extends Component {
login: PropTypes.string
}).isRequired,
user: userShape.isRequired,
accounts: PropTypes.shape({
available: PropTypes.array
}),
setErrors: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired,
resolve: PropTypes.func.isRequired,
@ -320,9 +323,15 @@ class PanelTransition extends Component {
}
getHeader({key, style, data}) {
const {Title, hasBackButton} = data;
const {Title} = data;
const {transformSpring} = style;
let {hasBackButton} = data;
if (typeof hasBackButton === 'function') {
hasBackButton = hasBackButton(this.props);
}
style = {
...this.getDefaultTransitionStyles(key, style),
opacity: 1 // reset default

View File

@ -1,6 +1,7 @@
import { routeActions } from 'react-router-redux';
import logger from 'services/logger';
import history from 'services/history';
import { updateUser, acceptRules as userAcceptRules } from 'components/user/actions';
import { authenticate, logoutAll } from 'components/accounts/actions';
import authentication from 'services/api/authentication';
@ -11,6 +12,25 @@ import dispatchBsod from 'components/ui/bsod/dispatchBsod';
export { updateUser } from 'components/user/actions';
export { authenticate, logoutAll as logout } from 'components/accounts/actions';
/**
* Reoutes user to the previous page if it is possible
*
* @param {string} fallbackUrl - an url to route user to if goBack is not possible
*
* @return {object} - action definition
*/
export function goBack(fallbackUrl = null) {
if (history.canGoBack()) {
return routeActions.goBack();
} else if (fallbackUrl) {
return routeActions.push(fallbackUrl);
}
return {
type: 'noop'
};
}
export function login({login = '', password = '', rememberMe = false}) {
const PASSWORD_REQUIRED = 'error.password_required';
const LOGIN_REQUIRED = 'error.login_required';

View File

@ -2,5 +2,6 @@
"chooseAccountTitle": "Choose an account",
"addAccount": "Log into another account",
"logoutAll": "Log out from all accounts",
"createNewAccount": "Create new account",
"description": "You have logged in into multiple accounts. Please choose the one, you want to use to authorize {appName}"
}

View File

@ -10,7 +10,11 @@ export default factory({
},
links: [
{
label: messages.logoutAll
label: messages.createNewAccount
},
{
label: messages.logoutAll,
payload: {logout: true}
}
]
});

View File

@ -1,5 +1,3 @@
import React from 'react';
import { Input } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
@ -8,6 +6,9 @@ import messages from './Login.intl.json';
export default class LoginBody extends BaseAuthBody {
static displayName = 'LoginBody';
static panelId = 'login';
static hasGoBack = (state) => {
return !state.user.isGuest;
};
autoFocusField = 'login';

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';

View File

@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';
import Helmet from 'react-helmet';
import { Input, Button, Checkbox, Form, FormModel } from 'components/ui/form';

View File

@ -14,6 +14,9 @@ import bsodFactory from 'components/ui/bsod/factory';
import loader from 'services/loader';
import logger from 'services/logger';
import font from 'services/font';
import history from 'services/history';
history.init();
logger.init({
sentryCdn: window.SENTRY_CDN

View File

@ -1,6 +1,7 @@
import AbstractState from './AbstractState';
import LoginState from './LoginState';
import CompleteState from './CompleteState';
import RegisterState from './RegisterState';
export default class ChooseAccountState extends AbstractState {
enter(context) {
@ -8,9 +9,6 @@ export default class ChooseAccountState extends AbstractState {
}
resolve(context, payload) {
// do not ask again after user adds account, or chooses an existed one
context.run('setAccountSwitcher', false);
if (payload.id) {
context.setState(new CompleteState());
} else {
@ -19,7 +17,16 @@ export default class ChooseAccountState extends AbstractState {
}
}
reject(context) {
context.run('logout');
/**
* @param {object} context
* @param {object} payload
* @param {bool} [payload.logout=false]
*/
reject(context, payload = {}) {
if (payload.logout) {
context.run('logout');
} else {
context.setState(new RegisterState());
}
}
}

View File

@ -17,7 +17,7 @@ export default class CompleteState extends AbstractState {
}
enter(context) {
const {auth = {}, user, accounts} = context.getState();
const {auth = {}, user} = context.getState();
if (user.isGuest) {
context.setState(new LoginState());
@ -26,67 +26,75 @@ export default class CompleteState extends AbstractState {
} else if (user.shouldAcceptRules) {
context.setState(new AcceptRulesState());
} else if (auth.oauth && auth.oauth.clientId) {
let isSwitcherEnabled = auth.isSwitcherEnabled;
if (auth.oauth.loginHint) {
const account = accounts.available.filter((account) =>
account.id === auth.oauth.loginHint * 1
|| account.email === auth.oauth.loginHint
|| account.username === auth.oauth.loginHint
)[0];
if (account) {
// disable switching, because we are know the account, user must be authorized with
context.run('setAccountSwitcher', false);
isSwitcherEnabled = false;
if (account.id !== accounts.active.id) {
// lets switch user to an account, that is needed for auth
return context.run('authenticate', account)
.then(() => context.setState(new CompleteState()));
}
}
}
if (isSwitcherEnabled
&& (accounts.available.length > 1
|| auth.oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE)
)
) {
context.setState(new ChooseAccountState());
} else if (auth.oauth.code) {
context.setState(new FinishState());
} else {
const data = {};
if (typeof this.isPermissionsAccepted !== 'undefined') {
data.accept = this.isPermissionsAccepted;
} else if (auth.oauth.acceptRequired || auth.oauth.prompt.includes(PROMPT_PERMISSIONS)) {
context.setState(new PermissionsState());
return;
}
// TODO: it seams that oAuthComplete may be a separate state
return context.run('oAuthComplete', data).then((resp) => {
// TODO: пусть в стейт попадает флаг или тип авторизации
// вместо волшебства над редирект урлой
if (resp.redirectUri.indexOf('static_page') === 0) {
context.setState(new FinishState());
} else {
return new Promise(() => {
// do not resolve promise to make loader visible and
// overcome app rendering
context.run('redirect', resp.redirectUri);
});
}
}, (resp) => {
if (resp.unauthorized) {
context.setState(new LoginState());
} else if (resp.acceptRequired) {
context.setState(new PermissionsState());
}
});
}
return this.processOAuth(context);
} else {
context.navigate('/');
}
}
processOAuth(context) {
const {auth = {}, accounts} = context.getState();
let isSwitcherEnabled = auth.isSwitcherEnabled;
const loginHint = auth.oauth.loginHint;
if (loginHint) {
const account = accounts.available.filter((account) =>
account.id === loginHint * 1
|| account.email === loginHint
|| account.username === loginHint
)[0];
if (account) {
// disable switching, because we are know the account, user must be authorized with
context.run('setAccountSwitcher', false);
isSwitcherEnabled = false;
if (account.id !== accounts.active.id) {
// lets switch user to an account, that is needed for auth
return context.run('authenticate', account)
.then(() => context.setState(new CompleteState()));
}
}
}
if (isSwitcherEnabled
&& (accounts.available.length > 1
|| auth.oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE)
)
) {
context.setState(new ChooseAccountState());
} else if (auth.oauth.code) {
context.setState(new FinishState());
} else {
const data = {};
if (typeof this.isPermissionsAccepted !== 'undefined') {
data.accept = this.isPermissionsAccepted;
} else if (auth.oauth.acceptRequired || auth.oauth.prompt.includes(PROMPT_PERMISSIONS)) {
context.setState(new PermissionsState());
return;
}
// TODO: it seams that oAuthComplete may be a separate state
return context.run('oAuthComplete', data).then((resp) => {
// TODO: пусть в стейт попадает флаг или тип авторизации
// вместо волшебства над редирект урлой
if (resp.redirectUri.indexOf('static_page') === 0) {
context.setState(new FinishState());
} else {
return new Promise(() => {
// do not resolve promise to make loader visible and
// overcome app rendering
context.run('redirect', resp.redirectUri);
});
}
}, (resp) => {
if (resp.unauthorized) {
context.setState(new LoginState());
} else if (resp.acceptRequired) {
context.setState(new PermissionsState());
}
});
}
}
}

View File

@ -7,15 +7,16 @@ export default class LoginState extends AbstractState {
enter(context) {
const {auth, user} = context.getState();
const isUserAddsSecondAccount = !user.isGuest
&& /login|password/.test(context.getRequest().path); // TODO: improve me
// 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 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
) {
} else if (user.isGuest || isUserAddsSecondAccount) {
context.navigate('/login');
} else {
// can not detect needed state. Delegating decision to the next state
context.setState(new PasswordState());
}
}
@ -25,4 +26,8 @@ export default class LoginState extends AbstractState {
.then(() => context.setState(new PasswordState()))
.catch((err = {}) => err.errors || logger.warn(err));
}
goBack(context) {
context.run('goBack', '/');
}
}

View File

@ -19,7 +19,7 @@ export default class PasswordState extends AbstractState {
resolve(context, {password, rememberMe}) {
const {auth: {login}} = context.getState();
context.run('login', {
return context.run('login', {
password,
rememberMe,
login

View File

@ -7,13 +7,7 @@ import ResendActivationState from './ResendActivationState';
export default class RegisterState extends AbstractState {
enter(context) {
const {user} = context.getState();
if (user.isGuest) {
context.navigate('/register');
} else {
context.setState(new CompleteState());
}
context.navigate('/register');
}
resolve(context, payload) {

17
src/services/history.js Normal file
View File

@ -0,0 +1,17 @@
/**
* A helper wrapper service around window.history
*/
export default {
init() {
this.initialLength = window.history.length;
},
/**
* @return {bool} - whether history.back() can be safetly called
*/
canGoBack() {
return document.referrer.includes(`${location.protocol}//${location.host}`)
|| this.initialLength < window.history.length;
}
};

View File

@ -3,21 +3,23 @@ import sinon from 'sinon';
import { routeActions } from 'react-router-redux';
import accounts from 'services/api/accounts';
import authentication from 'services/api/authentication';
import {
authenticate,
revoke,
add, ADD,
activate, ACTIVATE,
remove,
reset,
logoutAll,
logoutStrangers
} from 'components/accounts/actions';
import {
add, ADD,
activate, ACTIVATE,
remove,
reset
} from 'components/accounts/actions/pure-actions';
import { SET_LOCALE } from 'components/i18n/actions';
import { updateUser, setUser } from 'components/user/actions';
import { setAccountSwitcher } from 'components/auth/actions';
const account = {
id: 1,
@ -66,7 +68,7 @@ describe('components/accounts/actions', () => {
describe('#authenticate()', () => {
it('should request user state using token', () =>
authenticate(account)(dispatch).then(() =>
authenticate(account)(dispatch, getState).then(() =>
expect(authentication.validateToken, 'to have a call satisfying', [
{token: account.token, refreshToken: account.refreshToken}
])
@ -74,7 +76,7 @@ describe('components/accounts/actions', () => {
);
it(`dispatches ${ADD} action`, () =>
authenticate(account)(dispatch).then(() =>
authenticate(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
add(account)
])
@ -82,7 +84,7 @@ describe('components/accounts/actions', () => {
);
it(`dispatches ${ACTIVATE} action`, () =>
authenticate(account)(dispatch).then(() =>
authenticate(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
activate(account)
])
@ -90,7 +92,7 @@ describe('components/accounts/actions', () => {
);
it(`dispatches ${SET_LOCALE} action`, () =>
authenticate(account)(dispatch).then(() =>
authenticate(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
{type: SET_LOCALE, payload: {locale: 'be'}}
])
@ -98,7 +100,7 @@ describe('components/accounts/actions', () => {
);
it('should update user state', () =>
authenticate(account)(dispatch).then(() =>
authenticate(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
updateUser({...user, isGuest: false})
])
@ -106,7 +108,7 @@ describe('components/accounts/actions', () => {
);
it('resolves with account', () =>
authenticate(account)(dispatch).then((resp) =>
authenticate(account)(dispatch, getState).then((resp) =>
expect(resp, 'to equal', account)
)
);
@ -114,7 +116,7 @@ describe('components/accounts/actions', () => {
it('rejects when bad auth data', () => {
authentication.validateToken.returns(Promise.reject({}));
return expect(authenticate(account)(dispatch), 'to be rejected').then(() => {
return expect(authenticate(account)(dispatch, getState), 'to be rejected').then(() => {
expect(dispatch, 'to have a call satisfying', [
{payload: {isGuest: true}},
]);
@ -133,11 +135,37 @@ describe('components/accounts/actions', () => {
sessionStorage.removeItem(expectedKey);
return authenticate(account)(dispatch).then(() => {
return authenticate(account)(dispatch, getState).then(() => {
expect(sessionStorage.getItem(expectedKey), 'not to be null');
sessionStorage.removeItem(expectedKey);
});
});
describe('when user authenticated during oauth', () => {
beforeEach(() => {
getState.returns({
accounts: {
available: [],
active: null
},
user: {},
auth: {
oauth: {
clientId: 'ely.by',
prompt: []
}
}
});
});
it('should dispatch setAccountSwitcher', () =>
authenticate(account)(dispatch, getState).then(() =>
expect(dispatch, 'to have a call satisfying', [
setAccountSwitcher(false)
])
)
);
});
});
describe('#revoke()', () => {

View File

@ -2,9 +2,12 @@ import expect from 'unexpected';
import accounts from 'components/accounts/reducer';
import {
updateToken, add, remove, activate, reset,
ADD, REMOVE, ACTIVATE, UPDATE_TOKEN, RESET
updateToken
} from 'components/accounts/actions';
import {
add, remove, activate, reset,
ADD, REMOVE, ACTIVATE, UPDATE_TOKEN, RESET
} from 'components/accounts/actions/pure-actions';
const account = {
id: 1,

View File

@ -1,6 +1,7 @@
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
import CompleteState from 'services/authFlow/CompleteState';
import LoginState from 'services/authFlow/LoginState';
import RegisterState from 'services/authFlow/RegisterState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
@ -31,14 +32,12 @@ describe('ChooseAccountState', () => {
describe('#resolve', () => {
it('should transition to complete if existed account was choosen', () => {
expectRun(mock, 'setAccountSwitcher', false);
expectState(mock, CompleteState);
state.resolve(context, {id: 123});
});
it('should transition to login if user wants to add new account', () => {
expectRun(mock, 'setAccountSwitcher', false);
expectNavigate(mock, '/login');
expectState(mock, LoginState);
@ -47,10 +46,16 @@ describe('ChooseAccountState', () => {
});
describe('#reject', () => {
it('should logout', () => {
expectRun(mock, 'logout');
it('should transition to register', () => {
expectState(mock, RegisterState);
state.reject(context);
});
it('should logout', () => {
expectRun(mock, 'logout');
state.reject(context, {logout: true});
});
});
});

View File

@ -1,4 +1,5 @@
import expect from 'unexpected';
import sinon from 'sinon';
import CompleteState from 'services/authFlow/CompleteState';
import LoginState from 'services/authFlow/LoginState';
@ -6,6 +7,7 @@ import ActivationState from 'services/authFlow/ActivationState';
import AcceptRulesState from 'services/authFlow/AcceptRulesState';
import FinishState from 'services/authFlow/FinishState';
import PermissionsState from 'services/authFlow/PermissionsState';
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
@ -133,9 +135,144 @@ describe('CompleteState', () => {
state.enter(context);
});
it('should transition to permissions state if prompt=consent', () => {
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
auth: {
oauth: {
clientId: 'ely.by',
prompt: ['consent']
}
}
});
expectState(mock, PermissionsState);
state.enter(context);
});
it('should transition to ChooseAccountState if user has multiple accs and switcher enabled', () => {
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
{id: 1},
{id: 2}
],
active: {
id: 1
}
},
auth: {
isSwitcherEnabled: true,
oauth: {
clientId: 'ely.by',
prompt: []
}
}
});
expectState(mock, ChooseAccountState);
state.enter(context);
});
it('should NOT transition to ChooseAccountState if user has multiple accs and switcher disabled', () => {
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
{id: 1},
{id: 2}
],
active: {
id: 1
}
},
auth: {
isSwitcherEnabled: false,
oauth: {
clientId: 'ely.by',
prompt: []
}
}
});
expectRun(mock, 'oAuthComplete', {})
.returns({then() {}});
state.enter(context);
});
it('should transition to ChooseAccountState if prompt=select_account and switcher enabled', () => {
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
{id: 1}
],
active: {
id: 1
}
},
auth: {
isSwitcherEnabled: true,
oauth: {
clientId: 'ely.by',
prompt: ['select_account']
}
}
});
expectState(mock, ChooseAccountState);
state.enter(context);
});
it('should NOT transition to ChooseAccountState if prompt=select_account and switcher disabled', () => {
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
{id: 1}
],
active: {
id: 1
}
},
auth: {
isSwitcherEnabled: false,
oauth: {
clientId: 'ely.by',
prompt: ['select_account']
}
}
});
expectRun(mock, 'oAuthComplete', {})
.returns({then() {}});
state.enter(context);
});
});
describe('oAuthComplete', () => {
describe('when user completes oauth', () => {
it('should run oAuthComplete', () => {
context.getState.returns({
user: {
@ -185,7 +322,7 @@ describe('CompleteState', () => {
state.enter(context);
});
it('should transition run redirect by default', () => {
it('should run redirect by default', () => {
const expectedUrl = 'foo/bar';
const promise = Promise.resolve({redirectUri: expectedUrl});
@ -261,6 +398,122 @@ describe('CompleteState', () => {
it('should transition to permissions state if rejected with acceptRequired', () =>
testOAuth('reject', {acceptRequired: true}, PermissionsState)
);
describe('when loginHint is set', () => {
const testSuccessLoginHint = (field) => {
const account = {
id: 9,
email: 'some@email.com',
username: 'thatUsername'
};
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
account
],
active: {
id: 100
}
},
auth: {
oauth: {
clientId: 'ely.by',
loginHint: account[field],
prompt: []
}
}
});
expectRun(mock, 'setAccountSwitcher', false);
expectRun(mock, 'authenticate', account)
.returns(Promise.resolve());
expectState(mock, CompleteState);
return expect(state.enter(context), 'to be fulfilled');
};
it('should authenticate account if id matches', () =>
testSuccessLoginHint('id')
);
it('should authenticate account if email matches', () =>
testSuccessLoginHint('email')
);
it('should authenticate account if username matches', () =>
testSuccessLoginHint('username')
);
it('should not authenticate if account is already authenticated', () => {
const account = {
id: 9,
email: 'some@email.com',
username: 'thatUsername'
};
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [
account
],
active: account
},
auth: {
oauth: {
clientId: 'ely.by',
loginHint: account.id,
prompt: []
}
}
});
expectRun(mock, 'setAccountSwitcher', false);
expectRun(mock, 'oAuthComplete', {})
.returns({then: () => Promise.resolve()});
return expect(state.enter(context), 'to be fulfilled');
});
it('should not authenticate if account was not found and continue auth', () => {
const account = {
id: 9,
email: 'some@email.com',
username: 'thatUsername'
};
context.getState.returns({
user: {
isActive: true,
isGuest: false
},
accounts: {
available: [{id: 1}],
active: {id: 1}
},
auth: {
oauth: {
clientId: 'ely.by',
loginHint: account.id,
prompt: []
}
}
});
expectRun(mock, 'oAuthComplete', {})
.returns({then: () => Promise.resolve()});
return expect(state.enter(context), 'to be fulfilled');
});
});
});
describe('permissions accept', () => {

View File

@ -81,4 +81,12 @@ describe('LoginState', () => {
return promise.catch(mock.verify.bind(mock));
});
});
describe('#goBack', () => {
it('should return to previous page', () => {
expectRun(mock, 'goBack', '/');
state.goBack(context);
});
});
});

View File

@ -1,3 +1,4 @@
import expect from 'unexpected';
import sinon from 'sinon';
import PasswordState from 'services/authFlow/PasswordState';
@ -69,27 +70,11 @@ describe('PasswordState', () => {
rememberMe: expectedRememberMe,
})
).returns(Promise.resolve());
state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe});
});
it('should transition to complete state on successfull login', () => {
const promise = Promise.resolve();
const expectedLogin = 'login';
const expectedPassword = 'password';
context.getState.returns({
auth: {
login: expectedLogin
}
});
mock.expects('run').returns(promise);
expectState(mock, CompleteState);
state.resolve(context, {password: expectedPassword});
const payload = {password: expectedPassword, rememberMe: expectedRememberMe};
return promise;
return expect(state.resolve(context, payload), 'to be fulfilled');
});
});

View File

@ -34,16 +34,6 @@ describe('RegisterState', () => {
state.enter(context);
});
it('should transition to complete if not guest', () => {
context.getState.returns({
user: {isGuest: false}
});
expectState(mock, CompleteState);
state.enter(context);
});
});
describe('#resolve', () => {

View File

@ -2,6 +2,8 @@
* A helpers for testing states in isolation from AuthFlow
*/
import sinon from 'sinon';
export function bootstrap() {
const context = {
getState: sinon.stub(),
@ -28,9 +30,9 @@ export function expectState(mock, state) {
export function expectNavigate(mock, route, options) {
if (options) {
return mock.expects('navigate').once().withExactArgs(route, sinon.match(options));
} else {
return mock.expects('navigate').once().withExactArgs(route);
}
return mock.expects('navigate').once().withExactArgs(route);
}
export function expectRun(mock, ...args) {