mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-26 23:10:20 +05:30
Merge branch '245-multiacc-improvement' into develop
Conflicts: frontend/src/index.js
This commit is contained in:
commit
477b79918f
@ -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
|
||||
};
|
||||
}
|
||||
|
69
src/components/accounts/actions/pure-actions.js
Normal file
69
src/components/accounts/actions/pure-actions.js
Normal 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
|
||||
};
|
||||
}
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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}"
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ export default factory({
|
||||
},
|
||||
links: [
|
||||
{
|
||||
label: messages.logoutAll
|
||||
label: messages.createNewAccount
|
||||
},
|
||||
{
|
||||
label: messages.logoutAll,
|
||||
payload: {logout: true}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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', '/');
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
17
src/services/history.js
Normal 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;
|
||||
}
|
||||
};
|
@ -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()', () => {
|
||||
|
@ -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,
|
||||
|
@ -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});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user