mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-26 23:10:20 +05:30
Merge branch '246-auth-refactoring' into develop
This commit is contained in:
commit
befc058a3c
@ -1,16 +1,17 @@
|
||||
import { routeActions } from 'react-router-redux';
|
||||
|
||||
import authentication from 'services/api/authentication';
|
||||
import accounts from 'services/api/accounts';
|
||||
import { updateUser, logout } from 'components/user/actions';
|
||||
import { updateUser, setGuest } from 'components/user/actions';
|
||||
import { setLocale } from 'components/i18n/actions';
|
||||
import logger from 'services/logger';
|
||||
|
||||
/**
|
||||
* @typedef {object} Account
|
||||
* @property {string} account.id
|
||||
* @property {string} account.username
|
||||
* @property {string} account.email
|
||||
* @property {string} account.token
|
||||
* @property {string} account.refreshToken
|
||||
* @property {string} id
|
||||
* @property {string} username
|
||||
* @property {string} email
|
||||
* @property {string} token
|
||||
* @property {string} refreshToken
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -23,28 +24,27 @@ import logger from 'services/logger';
|
||||
export function authenticate({token, refreshToken}) {
|
||||
return (dispatch) =>
|
||||
authentication.validateToken({token, refreshToken})
|
||||
.catch(() => {
|
||||
// TODO: log this case
|
||||
dispatch(logout());
|
||||
.catch((resp) => {
|
||||
logger.warn('Error validating token during auth', {
|
||||
resp
|
||||
});
|
||||
|
||||
return Promise.reject();
|
||||
return dispatch(logoutAll())
|
||||
.then(() => Promise.reject());
|
||||
})
|
||||
.then(({token, refreshToken}) =>
|
||||
accounts.current({token})
|
||||
.then((user) => ({
|
||||
user: {
|
||||
isGuest: false,
|
||||
...user
|
||||
},
|
||||
account: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
token,
|
||||
refreshToken
|
||||
}
|
||||
}))
|
||||
)
|
||||
.then(({token, refreshToken, user}) => ({
|
||||
user: {
|
||||
isGuest: false,
|
||||
...user
|
||||
},
|
||||
account: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
token,
|
||||
refreshToken
|
||||
}
|
||||
}))
|
||||
.then(({user, account}) => {
|
||||
dispatch(add(account));
|
||||
dispatch(activate(account));
|
||||
@ -80,17 +80,23 @@ export function revoke(account) {
|
||||
});
|
||||
}
|
||||
|
||||
return dispatch(logout());
|
||||
return dispatch(logoutAll());
|
||||
};
|
||||
}
|
||||
|
||||
export function logoutAll() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setGuest());
|
||||
|
||||
const {accounts: {available}} = getState();
|
||||
|
||||
available.forEach((account) => authentication.logout(account));
|
||||
|
||||
dispatch(reset());
|
||||
|
||||
dispatch(routeActions.push('/login'));
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
@ -104,24 +110,28 @@ export function logoutAll() {
|
||||
*/
|
||||
export function logoutStrangers() {
|
||||
return (dispatch, getState) => {
|
||||
const {accounts: {available}} = getState();
|
||||
const {accounts: {available, active}} = getState();
|
||||
|
||||
const isStranger = ({refreshToken, id}) => !refreshToken && !sessionStorage.getItem(`stranger${id}`);
|
||||
|
||||
const accountToReplace = available.filter((account) => !isStranger(account))[0];
|
||||
if (available.some(isStranger)) {
|
||||
const accountToReplace = available.filter((account) => !isStranger(account))[0];
|
||||
|
||||
if (accountToReplace) {
|
||||
available.filter(isStranger)
|
||||
.forEach((account) => {
|
||||
dispatch(remove(account));
|
||||
authentication.logout(account);
|
||||
});
|
||||
if (accountToReplace) {
|
||||
available.filter(isStranger)
|
||||
.forEach((account) => {
|
||||
dispatch(remove(account));
|
||||
authentication.logout(account);
|
||||
});
|
||||
|
||||
return dispatch(authenticate(accountToReplace));
|
||||
if (isStranger(active)) {
|
||||
return dispatch(authenticate(accountToReplace));
|
||||
}
|
||||
} else {
|
||||
return dispatch(logoutAll());
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(logout());
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { routeActions } from 'react-router-redux';
|
||||
|
||||
import { updateUser, logout, acceptRules as userAcceptRules } from 'components/user/actions';
|
||||
import { authenticate } from 'components/accounts/actions';
|
||||
import logger from 'services/logger';
|
||||
import { updateUser, acceptRules as userAcceptRules } from 'components/user/actions';
|
||||
import { authenticate, logoutAll } from 'components/accounts/actions';
|
||||
import authentication from 'services/api/authentication';
|
||||
import oauth from 'services/api/oauth';
|
||||
import signup from 'services/api/signup';
|
||||
import dispatchBsod from 'components/ui/bsod/dispatchBsod';
|
||||
|
||||
export { updateUser } from 'components/user/actions';
|
||||
export { authenticate, logoutAll as logout } from 'components/accounts/actions';
|
||||
|
||||
export function login({login = '', password = '', rememberMe = false}) {
|
||||
const PASSWORD_REQUIRED = 'error.password_required';
|
||||
const LOGIN_REQUIRED = 'error.login_required';
|
||||
@ -24,9 +28,9 @@ export function login({login = '', password = '', rememberMe = false}) {
|
||||
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
|
||||
return dispatch(needActivation());
|
||||
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
|
||||
// TODO: log this case to backend
|
||||
// return to the first step
|
||||
return dispatch(logout());
|
||||
logger.warn('No login on password panel');
|
||||
|
||||
return dispatch(logoutAll());
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,9 +148,6 @@ export function clearErrors() {
|
||||
return setErrors(null);
|
||||
}
|
||||
|
||||
export { logout, updateUser } from 'components/user/actions';
|
||||
export { authenticate } from 'components/accounts/actions';
|
||||
|
||||
/**
|
||||
* @param {object} oauthData
|
||||
* @param {string} oauthData.clientId
|
||||
|
@ -1,66 +1,18 @@
|
||||
import { PropTypes } from 'react';
|
||||
|
||||
const KEY_USER = 'user';
|
||||
|
||||
export default class User {
|
||||
/**
|
||||
* @param {object} [data] - plain object or jwt token or empty to load from storage
|
||||
*
|
||||
* @return {User}
|
||||
*/
|
||||
constructor(data) {
|
||||
if (!data) {
|
||||
return this.load();
|
||||
}
|
||||
|
||||
// TODO: strict value types validation
|
||||
|
||||
const defaults = {
|
||||
id: null,
|
||||
uuid: null,
|
||||
username: '',
|
||||
email: '',
|
||||
// will contain user's email or masked email
|
||||
// (e.g. ex**ple@em*il.c**) depending on what information user have already provided
|
||||
maskedEmail: '',
|
||||
avatar: '',
|
||||
lang: '',
|
||||
isActive: false,
|
||||
shouldAcceptRules: false, // whether user need to review updated rules
|
||||
passwordChangedAt: null,
|
||||
hasMojangUsernameCollision: false,
|
||||
|
||||
// frontend app specific attributes
|
||||
isGuest: true,
|
||||
goal: null, // the goal with wich user entered site
|
||||
|
||||
// TODO: remove me after migration to multy accs
|
||||
token: '',
|
||||
refreshToken: ''
|
||||
};
|
||||
|
||||
const user = Object.keys(defaults).reduce((user, key) => {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
user[key] = data[key];
|
||||
}
|
||||
|
||||
return user;
|
||||
}, defaults);
|
||||
|
||||
localStorage.setItem(KEY_USER, JSON.stringify(user));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
load() {
|
||||
try {
|
||||
return new User(JSON.parse(localStorage.getItem(KEY_USER)));
|
||||
} catch (error) {
|
||||
return new User({isGuest: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} User
|
||||
* @property {number} id
|
||||
* @property {string} uuid
|
||||
* @property {string} token
|
||||
* @property {string} username
|
||||
* @property {string} email
|
||||
* @property {string} avatar
|
||||
* @property {bool} isGuest
|
||||
* @property {bool} isActive
|
||||
* @property {number} passwordChangedAt - timestamp
|
||||
* @property {bool} hasMojangUsernameCollision
|
||||
*/
|
||||
export const userShape = PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
uuid: PropTypes.string,
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { routeActions } from 'react-router-redux';
|
||||
|
||||
import accounts from 'services/api/accounts';
|
||||
import { logoutAll } from 'components/accounts/actions';
|
||||
import { setLocale } from 'components/i18n/actions';
|
||||
|
||||
export const UPDATE = 'USER_UPDATE';
|
||||
@ -18,6 +15,20 @@ export function updateUser(payload) {
|
||||
};
|
||||
}
|
||||
|
||||
export const SET = 'USER_SET';
|
||||
/**
|
||||
* Replace current user's state with a new one
|
||||
*
|
||||
* @param {User} payload
|
||||
* @return {object} - action definition
|
||||
*/
|
||||
export function setUser(payload) {
|
||||
return {
|
||||
type: SET,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||
export function changeLang(lang) {
|
||||
return (dispatch, getState) => dispatch(setLocale(lang))
|
||||
@ -37,32 +48,12 @@ export function changeLang(lang) {
|
||||
});
|
||||
}
|
||||
|
||||
export const SET = 'USER_SET';
|
||||
/**
|
||||
* Replace current user's state with a new one
|
||||
*
|
||||
* @param {User} payload
|
||||
* @return {object} - action definition
|
||||
*/
|
||||
export function setUser(payload) {
|
||||
return {
|
||||
type: SET,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function setGuest() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setUser({
|
||||
lang: getState().user.lang,
|
||||
isGuest: true
|
||||
}));
|
||||
|
||||
dispatch(logoutAll());
|
||||
|
||||
dispatch(routeActions.push('/login'));
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getJwtPayload } from 'functions';
|
||||
import authentication from 'services/api/authentication';
|
||||
import { updateToken } from 'components/accounts/actions';
|
||||
import { logout } from '../actions';
|
||||
import logger from 'services/logger';
|
||||
import { updateToken, logoutAll } from 'components/accounts/actions';
|
||||
|
||||
/**
|
||||
* Ensures, that all user's requests have fresh access token
|
||||
@ -14,7 +15,7 @@ import { logout } from '../actions';
|
||||
export default function refreshTokenMiddleware({dispatch, getState}) {
|
||||
return {
|
||||
before(req) {
|
||||
const {user, accounts} = getState();
|
||||
const {accounts} = getState();
|
||||
|
||||
let refreshToken;
|
||||
let token;
|
||||
@ -24,25 +25,25 @@ export default function refreshTokenMiddleware({dispatch, getState}) {
|
||||
if (accounts.active) {
|
||||
token = accounts.active.token;
|
||||
refreshToken = accounts.active.refreshToken;
|
||||
} else { // #legacy token
|
||||
token = user.token;
|
||||
refreshToken = user.refreshToken;
|
||||
}
|
||||
|
||||
if (!token || req.options.token || isRefreshTokenRequest) {
|
||||
return req;
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
try {
|
||||
const SAFETY_FACTOR = 300; // ask new token earlier to overcome time dissynchronization problem
|
||||
const jwt = getJWTPayload(token);
|
||||
const jwt = getJwtPayload(token);
|
||||
|
||||
if (jwt.exp - SAFETY_FACTOR < Date.now() / 1000) {
|
||||
return requestAccessToken(refreshToken, dispatch).then(() => req);
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error('Bad token', err); // TODO: it would be cool to log such things to backend
|
||||
return dispatch(logout()).then(() => req);
|
||||
logger.warn('Refresh token error: bad token', {
|
||||
token
|
||||
});
|
||||
|
||||
return dispatch(logoutAll()).then(() => req);
|
||||
}
|
||||
|
||||
return Promise.resolve(req);
|
||||
@ -50,15 +51,15 @@ export default function refreshTokenMiddleware({dispatch, getState}) {
|
||||
|
||||
catch(resp, req, restart) {
|
||||
if (resp && resp.status === 401 && !req.options.token) {
|
||||
const {user, accounts} = getState();
|
||||
const {refreshToken} = accounts.active ? accounts.active : user;
|
||||
const {accounts} = getState();
|
||||
const {refreshToken} = accounts.active || {};
|
||||
|
||||
if (resp.message === 'Token expired' && refreshToken) {
|
||||
// request token and retry
|
||||
return requestAccessToken(refreshToken, dispatch).then(restart);
|
||||
}
|
||||
|
||||
return dispatch(logout()).then(() => Promise.reject(resp));
|
||||
return dispatch(logoutAll()).then(() => Promise.reject(resp));
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
@ -76,20 +77,7 @@ function requestAccessToken(refreshToken, dispatch) {
|
||||
|
||||
return promise
|
||||
.then(({token}) => dispatch(updateToken(token)))
|
||||
.catch(() => dispatch(logout()));
|
||||
.catch(() => dispatch(logoutAll()));
|
||||
}
|
||||
|
||||
|
||||
function getJWTPayload(jwt) {
|
||||
const parts = (jwt || '').split('.');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid jwt token');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(parts[1]));
|
||||
} catch (err) {
|
||||
throw new Error('Can not decode jwt token');
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,28 @@
|
||||
import { UPDATE, SET, CHANGE_LANG } from './actions';
|
||||
|
||||
import User from './User';
|
||||
|
||||
// TODO: возможно есть смысл инитить обьект User снаружи, так как редусер не должен столько знать
|
||||
const defaults = {
|
||||
id: null,
|
||||
uuid: null,
|
||||
username: '',
|
||||
email: '',
|
||||
// will contain user's email or masked email
|
||||
// (e.g. ex**ple@em*il.c**) depending on what information user have already provided
|
||||
maskedEmail: '',
|
||||
avatar: '',
|
||||
lang: '',
|
||||
isActive: false,
|
||||
shouldAcceptRules: false, // whether user need to review updated rules
|
||||
passwordChangedAt: null,
|
||||
hasMojangUsernameCollision: false,
|
||||
|
||||
// frontend specific attributes
|
||||
isGuest: true,
|
||||
goal: null // the goal with wich user entered site
|
||||
};
|
||||
|
||||
export default function user(
|
||||
state = new User(),
|
||||
state = null,
|
||||
{type, payload = null}
|
||||
) {
|
||||
switch (type) {
|
||||
@ -13,25 +31,32 @@ export default function user(
|
||||
throw new Error('payload.lang is required for user reducer');
|
||||
}
|
||||
|
||||
return new User({
|
||||
return {
|
||||
...state,
|
||||
lang: payload.lang
|
||||
});
|
||||
};
|
||||
|
||||
case UPDATE:
|
||||
if (!payload) {
|
||||
throw new Error('payload is required for user reducer');
|
||||
}
|
||||
|
||||
return new User({
|
||||
return {
|
||||
...state,
|
||||
...payload
|
||||
});
|
||||
};
|
||||
|
||||
case SET:
|
||||
return new User(payload || {});
|
||||
payload = payload || {};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...payload
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
return state || {
|
||||
...defaults
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -61,3 +61,24 @@ export const rAF = window.requestAnimationFrame
|
||||
* @param {bool} [immediate=false] - whether to execute at the beginning
|
||||
*/
|
||||
export debounce from 'debounce';
|
||||
|
||||
/**
|
||||
* @param {string} jwt
|
||||
*
|
||||
* @throws {Error} If can not decode token
|
||||
*
|
||||
* @return {object} - decoded jwt payload
|
||||
*/
|
||||
export function getJwtPayload(jwt) {
|
||||
const parts = (jwt || '').split('.');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid jwt token');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(parts[1]));
|
||||
} catch (err) {
|
||||
throw new Error('Can not decode jwt token');
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ const authentication = {
|
||||
* @param {string} options.refreshToken
|
||||
*
|
||||
* @return {Promise} - resolves with options.token or with a new token
|
||||
* if it was refreshed
|
||||
* if it was refreshed. As a side effect the response
|
||||
* will have a `user` field with current user data
|
||||
*/
|
||||
validateToken({token, refreshToken}) {
|
||||
return new Promise((resolve) => {
|
||||
@ -66,11 +67,14 @@ const authentication = {
|
||||
resolve();
|
||||
})
|
||||
.then(() => accounts.current({token}))
|
||||
.then(() => ({token, refreshToken}))
|
||||
.then((user) => ({token, refreshToken, user}))
|
||||
.catch((resp) => {
|
||||
if (resp.message === 'Token expired') {
|
||||
return authentication.requestToken(refreshToken)
|
||||
.then(({token}) => ({token, refreshToken}));
|
||||
.then(({token}) =>
|
||||
accounts.current({token})
|
||||
.then((user) => ({token, refreshToken, user}))
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import expect from 'unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { routeActions } from 'react-router-redux';
|
||||
|
||||
import accounts from 'services/api/accounts';
|
||||
import authentication from 'services/api/authentication';
|
||||
import {
|
||||
@ -15,7 +17,7 @@ import {
|
||||
} from 'components/accounts/actions';
|
||||
import { SET_LOCALE } from 'components/i18n/actions';
|
||||
|
||||
import { updateUser } from 'components/user/actions';
|
||||
import { updateUser, setUser } from 'components/user/actions';
|
||||
|
||||
const account = {
|
||||
id: 1,
|
||||
@ -43,30 +45,30 @@ describe('components/accounts/actions', () => {
|
||||
getState = sinon.stub().named('store.getState');
|
||||
|
||||
getState.returns({
|
||||
accounts: [],
|
||||
accounts: {
|
||||
available: [],
|
||||
active: null
|
||||
},
|
||||
user: {}
|
||||
});
|
||||
|
||||
sinon.stub(authentication, 'validateToken').named('authentication.validateToken');
|
||||
authentication.validateToken.returns(Promise.resolve({
|
||||
token: account.token,
|
||||
refreshToken: account.refreshToken
|
||||
refreshToken: account.refreshToken,
|
||||
user
|
||||
}));
|
||||
|
||||
sinon.stub(accounts, 'current').named('accounts.current');
|
||||
accounts.current.returns(Promise.resolve(user));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
authentication.validateToken.restore();
|
||||
accounts.current.restore();
|
||||
});
|
||||
|
||||
describe('#authenticate()', () => {
|
||||
it('should request user state using token', () =>
|
||||
authenticate(account)(dispatch).then(() =>
|
||||
expect(accounts.current, 'to have a call satisfying', [
|
||||
{token: account.token}
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||
{token: account.token, refreshToken: account.refreshToken}
|
||||
])
|
||||
)
|
||||
);
|
||||
@ -110,17 +112,23 @@ describe('components/accounts/actions', () => {
|
||||
);
|
||||
|
||||
it('rejects when bad auth data', () => {
|
||||
accounts.current.returns(Promise.reject({}));
|
||||
authentication.validateToken.returns(Promise.reject({}));
|
||||
|
||||
return expect(authenticate(account)(dispatch), 'to be rejected').then(() =>
|
||||
expect(dispatch, 'was not called')
|
||||
);
|
||||
return expect(authenticate(account)(dispatch), 'to be rejected').then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
{payload: {isGuest: true}},
|
||||
]);
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
reset()
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks user as stranger, if there is no refreshToken', () => {
|
||||
const expectedKey = `stranger${account.id}`;
|
||||
authentication.validateToken.returns(Promise.resolve({
|
||||
token: account.token
|
||||
token: account.token,
|
||||
user
|
||||
}));
|
||||
|
||||
sessionStorage.removeItem(expectedKey);
|
||||
@ -257,6 +265,25 @@ describe('components/accounts/actions', () => {
|
||||
reset()
|
||||
]);
|
||||
});
|
||||
|
||||
it('should redirect to /login', () =>
|
||||
logoutAll()(dispatch, getState).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
routeActions.push('/login')
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
it('should change user to guest', () =>
|
||||
logoutAll()(dispatch, getState).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({
|
||||
lang: user.lang,
|
||||
isGuest: true
|
||||
})
|
||||
]);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('#logoutStrangers', () => {
|
||||
@ -274,7 +301,7 @@ describe('components/accounts/actions', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: account,
|
||||
active: foreignAccount,
|
||||
available: [account, foreignAccount, foreignAccount2]
|
||||
},
|
||||
user
|
||||
@ -316,6 +343,37 @@ describe('components/accounts/actions', () => {
|
||||
)
|
||||
);
|
||||
|
||||
it('should not activate another account if active account is already not a stranger', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: account,
|
||||
available: [account, foreignAccount]
|
||||
},
|
||||
user
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState)
|
||||
.then(() =>
|
||||
expect(dispatch, 'was always called with',
|
||||
expect.it('not to satisfy', activate(account)))
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch if no strangers', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: account,
|
||||
available: [account]
|
||||
},
|
||||
user
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState)
|
||||
.then(() =>
|
||||
expect(dispatch, 'was not called')
|
||||
);
|
||||
});
|
||||
|
||||
describe('when all accounts are strangers', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
@ -341,7 +399,7 @@ describe('components/accounts/actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an stranger has a mark in sessionStorage', () => {
|
||||
describe('when a stranger has a mark in sessionStorage', () => {
|
||||
const key = `stranger${foreignAccount.id}`;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -355,12 +413,9 @@ describe('components/accounts/actions', () => {
|
||||
});
|
||||
|
||||
it('should not log out', () =>
|
||||
expect(dispatch, 'to have calls satisfying', [
|
||||
[expect.it('not to equal', {payload: foreignAccount})],
|
||||
// for some reason it says, that dispatch(authenticate(...))
|
||||
// must be removed if only one args assertion is listed :(
|
||||
[expect.it('not to equal', {payload: foreignAccount})]
|
||||
])
|
||||
expect(dispatch, 'was always called with',
|
||||
expect.it('not to equal', {payload: foreignAccount})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,134 +1,2 @@
|
||||
import expect from 'unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { routeActions } from 'react-router-redux';
|
||||
|
||||
import request from 'services/request';
|
||||
import { reset, RESET } from 'components/accounts/actions';
|
||||
|
||||
import {
|
||||
logout,
|
||||
setUser
|
||||
} from 'components/user/actions';
|
||||
|
||||
|
||||
describe('components/user/actions', () => {
|
||||
const getState = sinon.stub().named('store.getState');
|
||||
const dispatch = sinon.spy((arg) =>
|
||||
typeof arg === 'function' ? arg(dispatch, getState) : arg
|
||||
).named('store.dispatch');
|
||||
|
||||
const callThunk = function(fn, ...args) {
|
||||
const thunk = fn(...args);
|
||||
|
||||
return thunk(dispatch, getState);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch.reset();
|
||||
getState.reset();
|
||||
getState.returns({});
|
||||
sinon.stub(request, 'get').named('request.get');
|
||||
sinon.stub(request, 'post').named('request.post');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
request.get.restore();
|
||||
request.post.restore();
|
||||
});
|
||||
|
||||
describe('#logout()', () => {
|
||||
beforeEach(() => {
|
||||
request.post.returns(Promise.resolve());
|
||||
});
|
||||
|
||||
describe('user with jwt', () => {
|
||||
const token = 'iLoveRockNRoll';
|
||||
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
user: {
|
||||
lang: 'foo'
|
||||
},
|
||||
accounts: {
|
||||
active: {token},
|
||||
available: [{token}]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should post to /api/authentication/logout with user jwt', () =>
|
||||
callThunk(logout).then(() => {
|
||||
expect(request.post, 'to have a call satisfying', [
|
||||
'/api/authentication/logout', {}, {
|
||||
token: expect.it('not to be empty')
|
||||
}
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
testChangedToGuest();
|
||||
testAccountsReset();
|
||||
testRedirectedToLogin();
|
||||
});
|
||||
|
||||
describe('user without jwt', () => {
|
||||
// (a guest with partially filled user's state)
|
||||
// DEPRECATED
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
user: {
|
||||
lang: 'foo'
|
||||
},
|
||||
accounts: {
|
||||
active: null,
|
||||
available: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not post to /api/authentication/logout', () =>
|
||||
callThunk(logout).then(() => {
|
||||
expect(request.post, 'was not called');
|
||||
})
|
||||
);
|
||||
|
||||
testChangedToGuest();
|
||||
testAccountsReset();
|
||||
testRedirectedToLogin();
|
||||
});
|
||||
|
||||
function testChangedToGuest() {
|
||||
it('should change user to guest', () =>
|
||||
callThunk(logout).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({
|
||||
lang: 'foo',
|
||||
isGuest: true
|
||||
})
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function testRedirectedToLogin() {
|
||||
it('should redirect to /login', () =>
|
||||
callThunk(logout).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
routeActions.push('/login')
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function testAccountsReset() {
|
||||
it(`should dispatch ${RESET}`, () =>
|
||||
callThunk(logout).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
reset()
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import expect from 'unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware';
|
||||
|
||||
@ -75,9 +76,11 @@ describe('refreshTokenMiddleware', () => {
|
||||
const data = {url: '/refresh-token', options: {}};
|
||||
const resp = middleware.before(data);
|
||||
|
||||
expect(resp, 'to satisfy', data);
|
||||
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
return expect(resp, 'to be fulfilled with', data)
|
||||
.then(() =>
|
||||
expect(authentication.requestToken, 'was not called')
|
||||
);
|
||||
});
|
||||
|
||||
it('should not auto refresh token if options.token specified', () => {
|
||||
@ -142,40 +145,6 @@ describe('refreshTokenMiddleware', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when token expired legacy user', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: null,
|
||||
available: []
|
||||
},
|
||||
user: {
|
||||
token: expiredToken,
|
||||
refreshToken
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should request new token', () => {
|
||||
const data = {
|
||||
url: 'foo',
|
||||
options: {
|
||||
headers: {}
|
||||
}
|
||||
};
|
||||
|
||||
authentication.requestToken.returns(Promise.resolve({token: validToken}));
|
||||
|
||||
return middleware.before(data).then((resp) => {
|
||||
expect(resp, 'to satisfy', data);
|
||||
|
||||
expect(authentication.requestToken, 'to have a call satisfying', [
|
||||
refreshToken
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be applied if no token', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
@ -187,9 +156,10 @@ describe('refreshTokenMiddleware', () => {
|
||||
const data = {url: 'foo'};
|
||||
const resp = middleware.before(data);
|
||||
|
||||
expect(resp, 'to satisfy', data);
|
||||
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
return expect(resp, 'to be fulfilled with', data)
|
||||
.then(() =>
|
||||
expect(authentication.requestToken, 'was not called')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -290,25 +260,5 @@ describe('refreshTokenMiddleware', () => {
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy user.refreshToken', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: null
|
||||
},
|
||||
user: {refreshToken}
|
||||
});
|
||||
});
|
||||
|
||||
it('should request new token if expired', () =>
|
||||
middleware.catch(expiredResponse, {options: {}}, restart).then(() => {
|
||||
expect(authentication.requestToken, 'to have a call satisfying', [
|
||||
refreshToken
|
||||
]);
|
||||
expect(restart, 'was called');
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -40,11 +40,12 @@ describe('authentication api', () => {
|
||||
|
||||
describe('#validateToken()', () => {
|
||||
const validTokens = {token: 'foo', refreshToken: 'bar'};
|
||||
const user = {id: 1};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(accounts, 'current');
|
||||
|
||||
accounts.current.returns(Promise.resolve());
|
||||
accounts.current.returns(Promise.resolve(user));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -60,8 +61,11 @@ describe('authentication api', () => {
|
||||
})
|
||||
);
|
||||
|
||||
it('should resolve with both tokens', () =>
|
||||
expect(authentication.validateToken(validTokens), 'to be fulfilled with', validTokens)
|
||||
it('should resolve with both tokens and user object', () =>
|
||||
expect(authentication.validateToken(validTokens), 'to be fulfilled with', {
|
||||
...validTokens,
|
||||
user
|
||||
})
|
||||
);
|
||||
|
||||
it('rejects if token has a bad type', () =>
|
||||
@ -96,7 +100,7 @@ describe('authentication api', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(authentication, 'requestToken');
|
||||
|
||||
accounts.current.returns(Promise.reject(expiredResponse));
|
||||
accounts.current.onCall(0).returns(Promise.reject(expiredResponse));
|
||||
authentication.requestToken.returns(Promise.resolve({token: newToken}));
|
||||
});
|
||||
|
||||
@ -104,9 +108,9 @@ describe('authentication api', () => {
|
||||
authentication.requestToken.restore();
|
||||
});
|
||||
|
||||
it('resolves with new token', () =>
|
||||
it('resolves with new token and user object', () =>
|
||||
expect(authentication.validateToken(validTokens),
|
||||
'to be fulfilled with', {...validTokens, token: newToken}
|
||||
'to be fulfilled with', {...validTokens, token: newToken, user}
|
||||
)
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user