mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-07-03 05:25:41 +05:30
#48: add/remove accounts from account switcher. Allow authorized users to log in into another account
This commit is contained in:
parent
5c9a1bc953
commit
586cdfffe4
|
@ -10,20 +10,13 @@ import { Button } from 'components/ui/form';
|
||||||
import styles from './accountSwitcher.scss';
|
import styles from './accountSwitcher.scss';
|
||||||
import messages from './AccountSwitcher.intl.json';
|
import messages from './AccountSwitcher.intl.json';
|
||||||
|
|
||||||
const accounts = {
|
export class AccountSwitcher extends Component {
|
||||||
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 {
|
|
||||||
static displayName = 'AccountSwitcher';
|
static displayName = 'AccountSwitcher';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
switchAccount: PropTypes.func.isRequired,
|
||||||
|
removeAccount: PropTypes.func.isRequired,
|
||||||
|
onAfterAction: PropTypes.func, // called after each action performed
|
||||||
accounts: PropTypes.shape({ // TODO: accounts shape
|
accounts: PropTypes.shape({ // TODO: accounts shape
|
||||||
active: PropTypes.shape({
|
active: PropTypes.shape({
|
||||||
id: PropTypes.number
|
id: PropTypes.number
|
||||||
|
@ -43,7 +36,7 @@ export default class AccountSwitcher extends Component {
|
||||||
highlightActiveAccount: true,
|
highlightActiveAccount: true,
|
||||||
allowLogout: true,
|
allowLogout: true,
|
||||||
allowAdd: true,
|
allowAdd: true,
|
||||||
accounts
|
onAfterAction() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -66,7 +59,7 @@ export default class AccountSwitcher extends Component {
|
||||||
styles.accountIcon,
|
styles.accountIcon,
|
||||||
styles.activeAccountIcon,
|
styles.activeAccountIcon,
|
||||||
styles.accountIcon1
|
styles.accountIcon1
|
||||||
)}></div>
|
)} />
|
||||||
<div className={styles.activeAccountInfo}>
|
<div className={styles.activeAccountInfo}>
|
||||||
<div className={styles.activeAccountUsername}>
|
<div className={styles.activeAccountUsername}>
|
||||||
{accounts.active.username}
|
{accounts.active.username}
|
||||||
|
@ -81,7 +74,7 @@ export default class AccountSwitcher extends Component {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.link}>
|
<div className={styles.link}>
|
||||||
<a href="" className={styles.link}>
|
<a className={styles.link} onClick={this.onRemove(accounts.active)} href="#">
|
||||||
<Message {...messages.logout} />
|
<Message {...messages.logout} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,16 +83,19 @@ export default class AccountSwitcher extends Component {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{available.map((account, id) => (
|
{available.map((account, id) => (
|
||||||
<div className={classNames(styles.item, styles.accountSwitchItem)} key={account.id}>
|
<div className={classNames(styles.item, styles.accountSwitchItem)}
|
||||||
|
key={account.id}
|
||||||
|
onClick={this.onSwitch(account)}
|
||||||
|
>
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
styles.accountIcon,
|
styles.accountIcon,
|
||||||
styles[`accountIcon${id % 7 + (highlightActiveAccount ? 2 : 1)}`]
|
styles[`accountIcon${id % 7 + (highlightActiveAccount ? 2 : 1)}`]
|
||||||
)}></div>
|
)} />
|
||||||
|
|
||||||
{allowLogout ? (
|
{allowLogout ? (
|
||||||
<div className={styles.logoutIcon}></div>
|
<div className={styles.logoutIcon} onClick={this.onRemove(account)} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.nextIcon}></div>
|
<div className={styles.nextIcon} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.accountInfo}>
|
<div className={styles.accountInfo}>
|
||||||
|
@ -113,7 +109,7 @@ export default class AccountSwitcher extends Component {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{allowAdd ? (
|
{allowAdd ? (
|
||||||
<Link to="/login">
|
<Link to="/login" onClick={this.props.onAfterAction}>
|
||||||
<Button
|
<Button
|
||||||
color={COLOR_WHITE}
|
color={COLOR_WHITE}
|
||||||
block
|
block
|
||||||
|
@ -135,5 +131,29 @@ export default class AccountSwitcher extends Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSwitch = (account) => (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.props.switchAccount(account)
|
||||||
|
.then(() => this.props.onAfterAction());
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemove = (account) => (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.props.removeAccount(account)
|
||||||
|
.then(() => this.props.onAfterAction());
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { authenticate, revoke } from 'components/accounts/actions';
|
||||||
|
|
||||||
|
export default connect(({accounts}) => ({
|
||||||
|
accounts
|
||||||
|
}), {
|
||||||
|
switchAccount: authenticate,
|
||||||
|
removeAccount: revoke
|
||||||
|
})(AccountSwitcher);
|
||||||
|
|
|
@ -55,10 +55,11 @@ export function authenticate({token, refreshToken}) {
|
||||||
*/
|
*/
|
||||||
export function revoke(account) {
|
export function revoke(account) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(remove(account));
|
const accountToReplace = getState().accounts.available.find(({id}) => id !== account.id);
|
||||||
|
|
||||||
if (getState().accounts.length) {
|
if (accountToReplace) {
|
||||||
return dispatch(authenticate(getState().accounts[0]));
|
return dispatch(authenticate(accountToReplace))
|
||||||
|
.then(() => dispatch(remove(account)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(logout());
|
return dispatch(logout());
|
||||||
|
|
|
@ -26,9 +26,9 @@ export default function accounts(
|
||||||
throw new Error('Invalid or empty payload passed for accounts.add');
|
throw new Error('Invalid or empty payload passed for accounts.add');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.available.some((account) => account.id === payload.id)) {
|
state.available = state.available
|
||||||
state.available = state.available.concat(payload);
|
.filter((account) => account.id !== payload.id)
|
||||||
}
|
.concat(payload);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
|
|
|
@ -446,12 +446,28 @@ class PanelTransition extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state) => ({
|
export default connect((state) => {
|
||||||
user: state.user,
|
const {login} = state.auth;
|
||||||
auth: state.auth,
|
const user = {
|
||||||
resolve: authFlow.resolve.bind(authFlow),
|
...state.user,
|
||||||
reject: authFlow.reject.bind(authFlow)
|
isGuest: true,
|
||||||
}), {
|
email: '',
|
||||||
|
username: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/[@.]/.test(login)) {
|
||||||
|
user.email = login;
|
||||||
|
} else {
|
||||||
|
user.username = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
auth: state.auth,
|
||||||
|
resolve: authFlow.resolve.bind(authFlow),
|
||||||
|
reject: authFlow.reject.bind(authFlow)
|
||||||
|
};
|
||||||
|
}, {
|
||||||
clearErrors: actions.clearErrors,
|
clearErrors: actions.clearErrors,
|
||||||
setErrors: actions.setErrors
|
setErrors: actions.setErrors
|
||||||
})(PanelTransition);
|
})(PanelTransition);
|
||||||
|
|
|
@ -20,19 +20,7 @@ export function login({login = '', password = '', rememberMe = false}) {
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
if (resp.errors) {
|
if (resp.errors) {
|
||||||
if (resp.errors.password === PASSWORD_REQUIRED) {
|
if (resp.errors.password === PASSWORD_REQUIRED) {
|
||||||
let username = '';
|
return dispatch(setLogin(login));
|
||||||
let email = '';
|
|
||||||
|
|
||||||
if (/[@.]/.test(login)) {
|
|
||||||
email = login;
|
|
||||||
} else {
|
|
||||||
username = login;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(updateUser({
|
|
||||||
username,
|
|
||||||
email
|
|
||||||
}));
|
|
||||||
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
|
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
|
||||||
return dispatch(needActivation());
|
return dispatch(needActivation());
|
||||||
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
|
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
|
||||||
|
@ -126,7 +114,15 @@ export function resendActivation({email = '', captcha}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ERROR = 'error';
|
export const SET_LOGIN = 'auth:setLogin';
|
||||||
|
export function setLogin(login) {
|
||||||
|
return {
|
||||||
|
type: SET_LOGIN,
|
||||||
|
payload: login
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR = 'auth:error';
|
||||||
export function setErrors(errors) {
|
export function setErrors(errors) {
|
||||||
return {
|
return {
|
||||||
type: ERROR,
|
type: ERROR,
|
||||||
|
@ -309,7 +305,11 @@ function authHandler(dispatch) {
|
||||||
return (resp) => dispatch(authenticate({
|
return (resp) => dispatch(authenticate({
|
||||||
token: resp.access_token,
|
token: resp.access_token,
|
||||||
refreshToken: resp.refresh_token
|
refreshToken: resp.refresh_token
|
||||||
}));
|
})).then((resp) => {
|
||||||
|
dispatch(setLogin(null));
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function validationErrorsHandler(dispatch, repeatUrl) {
|
function validationErrorsHandler(dispatch, repeatUrl) {
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import { ERROR, SET_CLIENT, SET_OAUTH, SET_OAUTH_RESULT, SET_SCOPES, SET_LOADING_STATE, REQUIRE_PERMISSIONS_ACCEPT } from './actions';
|
import {
|
||||||
|
ERROR,
|
||||||
|
SET_CLIENT,
|
||||||
|
SET_OAUTH,
|
||||||
|
SET_OAUTH_RESULT,
|
||||||
|
SET_SCOPES,
|
||||||
|
SET_LOADING_STATE,
|
||||||
|
REQUIRE_PERMISSIONS_ACCEPT,
|
||||||
|
SET_LOGIN
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
login,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
client,
|
client,
|
||||||
|
@ -19,6 +29,24 @@ function error(
|
||||||
if (!error) {
|
if (!error) {
|
||||||
throw new Error('Expected payload with error');
|
throw new Error('Expected payload with error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(
|
||||||
|
state = null,
|
||||||
|
{type, payload = null}
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case SET_LOGIN:
|
||||||
|
if (payload !== null && typeof payload !== 'string') {
|
||||||
|
throw new Error('Expected payload with login string or null');
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -44,18 +44,16 @@ export default class LoggedInPanel extends Component {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={classNames(styles.accountSwitcherContainer)}>
|
<div className={classNames(styles.accountSwitcherContainer)}>
|
||||||
<AccountSwitcher skin="light" />
|
<AccountSwitcher skin="light" onAfterAction={this.toggleAccountSwitcher} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAccountSwitcher() {
|
toggleAccountSwitcher = () => this.setState({
|
||||||
this.setState({
|
isAccountSwitcherActive: !this.state.isAccountSwitcherActive
|
||||||
isAccountSwitcherActive: !this.state.isAccountSwitcherActive
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onExpandAccountSwitcher = (event) => {
|
onExpandAccountSwitcher = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
@ -3,11 +3,15 @@ import PasswordState from './PasswordState';
|
||||||
|
|
||||||
export default class LoginState extends AbstractState {
|
export default class LoginState extends AbstractState {
|
||||||
enter(context) {
|
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());
|
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');
|
context.navigate('/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import LoginState from './LoginState';
|
||||||
|
|
||||||
export default class PasswordState extends AbstractState {
|
export default class PasswordState extends AbstractState {
|
||||||
enter(context) {
|
enter(context) {
|
||||||
const {user} = context.getState();
|
const {auth} = context.getState();
|
||||||
|
|
||||||
if (user.isGuest) {
|
if (auth.login) {
|
||||||
context.navigate('/password');
|
context.navigate('/password');
|
||||||
} else {
|
} else {
|
||||||
context.setState(new CompleteState());
|
context.setState(new CompleteState());
|
||||||
|
@ -15,12 +15,12 @@ export default class PasswordState extends AbstractState {
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(context, {password, rememberMe}) {
|
resolve(context, {password, rememberMe}) {
|
||||||
const {user} = context.getState();
|
const {auth: {login}} = context.getState();
|
||||||
|
|
||||||
context.run('login', {
|
context.run('login', {
|
||||||
password,
|
password,
|
||||||
rememberMe,
|
rememberMe,
|
||||||
login: user.email || user.username
|
login
|
||||||
})
|
})
|
||||||
.then(() => context.setState(new CompleteState()));
|
.then(() => context.setState(new CompleteState()));
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export default class PasswordState extends AbstractState {
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack(context) {
|
goBack(context) {
|
||||||
context.run('logout');
|
context.run('setLogin', null);
|
||||||
context.setState(new LoginState());
|
context.setState(new LoginState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const user = {
|
||||||
lang: 'be'
|
lang: 'be'
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Accounts actions', () => {
|
describe('components/accounts/actions', () => {
|
||||||
let dispatch;
|
let dispatch;
|
||||||
let getState;
|
let getState;
|
||||||
|
|
||||||
|
@ -109,19 +109,15 @@ describe('Accounts actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#revoke()', () => {
|
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', () => {
|
it('should switch next account if available', () => {
|
||||||
const account2 = {...account, id: 2};
|
const account2 = {...account, id: 2};
|
||||||
|
|
||||||
getState.returns({
|
getState.returns({
|
||||||
accounts: [account]
|
accounts: {
|
||||||
|
active: account2,
|
||||||
|
available: [account]
|
||||||
|
},
|
||||||
|
user
|
||||||
});
|
});
|
||||||
|
|
||||||
return revoke(account2)(dispatch, getState).then(() => {
|
return revoke(account2)(dispatch, getState).then(() => {
|
||||||
|
@ -143,10 +139,15 @@ describe('Accounts actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should logout if no other accounts available', () => {
|
it('should logout if no other accounts available', () => {
|
||||||
|
getState.returns({
|
||||||
|
accounts: {
|
||||||
|
active: account,
|
||||||
|
available: []
|
||||||
|
},
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
revoke(account)(dispatch, getState).then(() => {
|
revoke(account)(dispatch, getState).then(() => {
|
||||||
expect(dispatch, 'to have a call satisfying', [
|
|
||||||
remove(account)
|
|
||||||
]);
|
|
||||||
expect(dispatch, 'to have a call satisfying', [
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
{payload: {isGuest: true}}
|
{payload: {isGuest: true}}
|
||||||
// updateUser({isGuest: true})
|
// updateUser({isGuest: true})
|
||||||
|
|
|
@ -45,11 +45,23 @@ describe('Accounts reducer', () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should not add the same account twice', () =>
|
it('should replace if account was added for the second time', () => {
|
||||||
expect(accounts({...initial, available: [account]}, add(account)), 'to satisfy', {
|
const outdatedAccount = {
|
||||||
available: [account]
|
...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', () => {
|
it('throws, when account is invalid', () => {
|
||||||
expect(() => accounts(initial, add()),
|
expect(() => accounts(initial, add()),
|
||||||
|
|
|
@ -10,7 +10,9 @@ import {
|
||||||
setOAuthRequest,
|
setOAuthRequest,
|
||||||
setScopes,
|
setScopes,
|
||||||
setOAuthCode,
|
setOAuthCode,
|
||||||
requirePermissionsAccept
|
requirePermissionsAccept,
|
||||||
|
login,
|
||||||
|
setLogin
|
||||||
} from 'components/auth/actions';
|
} from 'components/auth/actions';
|
||||||
|
|
||||||
const oauthData = {
|
const oauthData = {
|
||||||
|
@ -22,8 +24,8 @@ const oauthData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('components/auth/actions', () => {
|
describe('components/auth/actions', () => {
|
||||||
const dispatch = sinon.stub().named('dispatch');
|
const dispatch = sinon.stub().named('store.dispatch');
|
||||||
const getState = sinon.stub().named('getState');
|
const getState = sinon.stub().named('store.getState');
|
||||||
|
|
||||||
function callThunk(fn, ...args) {
|
function callThunk(fn, ...args) {
|
||||||
const thunk = fn(...args);
|
const thunk = fn(...args);
|
||||||
|
@ -67,21 +69,21 @@ describe('components/auth/actions', () => {
|
||||||
request.get.returns(Promise.resolve(resp));
|
request.get.returns(Promise.resolve(resp));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send get request to an api', () => {
|
it('should send get request to an api', () =>
|
||||||
return callThunk(oAuthValidate, oauthData).then(() => {
|
callThunk(oAuthValidate, oauthData).then(() => {
|
||||||
expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]);
|
expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]);
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
it('should dispatch setClient, setOAuthRequest and setScopes', () => {
|
it('should dispatch setClient, setOAuthRequest and setScopes', () =>
|
||||||
return callThunk(oAuthValidate, oauthData).then(() => {
|
callThunk(oAuthValidate, oauthData).then(() => {
|
||||||
expectDispatchCalls([
|
expectDispatchCalls([
|
||||||
[setClient(resp.client)],
|
[setClient(resp.client)],
|
||||||
[setOAuthRequest(resp.oAuth)],
|
[setOAuthRequest(resp.oAuth)],
|
||||||
[setScopes(resp.session.scopes)]
|
[setScopes(resp.session.scopes)]
|
||||||
]);
|
]);
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#oAuthComplete()', () => {
|
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')]
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
16
tests/components/auth/reducer.test.js
Normal file
16
tests/components/auth/reducer.test.js
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -68,7 +68,7 @@ describe('refreshTokenMiddleware', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not apply to refresh-token request', () => {
|
it('should not apply to refresh-token request', () => {
|
||||||
const data = {url: '/refresh-token'};
|
const data = {url: '/refresh-token', options: {}};
|
||||||
const resp = middleware.before(data);
|
const resp = middleware.before(data);
|
||||||
|
|
||||||
expect(resp, 'to satisfy', data);
|
expect(resp, 'to satisfy', data);
|
||||||
|
|
|
@ -47,6 +47,9 @@ describe('AuthFlow.functional', () => {
|
||||||
state.user = {
|
state.user = {
|
||||||
isGuest: true
|
isGuest: true
|
||||||
};
|
};
|
||||||
|
state.auth = {
|
||||||
|
login: null
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect guest / -> /login', () => {
|
it('should redirect guest / -> /login', () => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import LoginState from 'services/authFlow/LoginState';
|
import LoginState from 'services/authFlow/LoginState';
|
||||||
import PasswordState from 'services/authFlow/PasswordState';
|
import PasswordState from 'services/authFlow/PasswordState';
|
||||||
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
|
|
||||||
|
|
||||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||||
|
|
||||||
|
@ -24,7 +23,8 @@ describe('LoginState', () => {
|
||||||
describe('#enter', () => {
|
describe('#enter', () => {
|
||||||
it('should navigate to /login', () => {
|
it('should navigate to /login', () => {
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: {isGuest: true}
|
user: {isGuest: true},
|
||||||
|
auth: {login: null}
|
||||||
});
|
});
|
||||||
|
|
||||||
expectNavigate(mock, '/login');
|
expectNavigate(mock, '/login');
|
||||||
|
@ -32,22 +32,15 @@ describe('LoginState', () => {
|
||||||
state.enter(context);
|
state.enter(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
const testTransitionToPassword = (user) => {
|
it('should transition to password if login was set', () => {
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: user
|
user: {isGuest: true},
|
||||||
|
auth: {login: 'foo'}
|
||||||
});
|
});
|
||||||
|
|
||||||
expectState(mock, PasswordState);
|
expectState(mock, PasswordState);
|
||||||
|
|
||||||
state.enter(context);
|
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'});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,8 @@ describe('PasswordState', () => {
|
||||||
describe('#enter', () => {
|
describe('#enter', () => {
|
||||||
it('should navigate to /password', () => {
|
it('should navigate to /password', () => {
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: {isGuest: true}
|
user: {isGuest: true},
|
||||||
|
auth: {login: 'foo'}
|
||||||
});
|
});
|
||||||
|
|
||||||
expectNavigate(mock, '/password');
|
expectNavigate(mock, '/password');
|
||||||
|
@ -35,7 +36,8 @@ describe('PasswordState', () => {
|
||||||
|
|
||||||
it('should transition to complete if not guest', () => {
|
it('should transition to complete if not guest', () => {
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: {isGuest: false}
|
user: {isGuest: false},
|
||||||
|
auth: {login: null}
|
||||||
});
|
});
|
||||||
|
|
||||||
expectState(mock, CompleteState);
|
expectState(mock, CompleteState);
|
||||||
|
@ -45,42 +47,29 @@ describe('PasswordState', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#resolve', () => {
|
describe('#resolve', () => {
|
||||||
(function() {
|
it('should call login with login and password', () => {
|
||||||
const expectedLogin = 'login';
|
const expectedLogin = 'foo';
|
||||||
const expectedPassword = 'password';
|
const expectedPassword = 'bar';
|
||||||
const expectedRememberMe = true;
|
const expectedRememberMe = true;
|
||||||
|
|
||||||
const testWith = (user) => {
|
context.getState.returns({
|
||||||
it(`should call login with email or username and password. User: ${JSON.stringify(user)}`, () => {
|
auth: {
|
||||||
context.getState.returns({user});
|
login: expectedLogin
|
||||||
|
}
|
||||||
expectRun(
|
|
||||||
mock,
|
|
||||||
'login',
|
|
||||||
sinon.match({
|
|
||||||
login: expectedLogin,
|
|
||||||
password: expectedPassword,
|
|
||||||
rememberMe: expectedRememberMe,
|
|
||||||
})
|
|
||||||
).returns({then() {}});
|
|
||||||
|
|
||||||
state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
testWith({
|
|
||||||
email: expectedLogin
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWith({
|
expectRun(
|
||||||
username: expectedLogin
|
mock,
|
||||||
});
|
'login',
|
||||||
|
sinon.match({
|
||||||
|
login: expectedLogin,
|
||||||
|
password: expectedPassword,
|
||||||
|
rememberMe: expectedRememberMe,
|
||||||
|
})
|
||||||
|
).returns({then() {}});
|
||||||
|
|
||||||
testWith({
|
state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe});
|
||||||
email: expectedLogin,
|
});
|
||||||
username: expectedLogin
|
|
||||||
});
|
|
||||||
}());
|
|
||||||
|
|
||||||
it('should transition to complete state on successfull login', () => {
|
it('should transition to complete state on successfull login', () => {
|
||||||
const promise = Promise.resolve();
|
const promise = Promise.resolve();
|
||||||
|
@ -88,8 +77,8 @@ describe('PasswordState', () => {
|
||||||
const expectedPassword = 'password';
|
const expectedPassword = 'password';
|
||||||
|
|
||||||
context.getState.returns({
|
context.getState.returns({
|
||||||
user: {
|
auth: {
|
||||||
email: expectedLogin
|
login: expectedLogin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -111,8 +100,8 @@ describe('PasswordState', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#goBack', () => {
|
describe('#goBack', () => {
|
||||||
it('should transition to forgot password state', () => {
|
it('should transition to login state', () => {
|
||||||
expectRun(mock, 'logout');
|
expectRun(mock, 'setLogin', null);
|
||||||
expectState(mock, LoginState);
|
expectState(mock, LoginState);
|
||||||
|
|
||||||
state.goBack(context);
|
state.goBack(context);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user