mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-10-01 15:57:46 +05:30
#48: integrate accounts with app
This commit is contained in:
parent
8601da786c
commit
000ce71d3e
@ -1,6 +1,7 @@
|
|||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
import accounts from 'services/api/accounts';
|
import accounts from 'services/api/accounts';
|
||||||
import { updateUser, logout } from 'components/user/actions';
|
import { updateUser, logout } from 'components/user/actions';
|
||||||
|
import { setLocale } from 'components/i18n/actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} Account
|
* @typedef {object} Account
|
||||||
@ -35,9 +36,13 @@ export function authenticate({token, refreshToken}) {
|
|||||||
.then(({user, account}) => {
|
.then(({user, account}) => {
|
||||||
dispatch(add(account));
|
dispatch(add(account));
|
||||||
dispatch(activate(account));
|
dispatch(activate(account));
|
||||||
dispatch(updateUser(user));
|
dispatch(updateUser({
|
||||||
|
isGuest: false,
|
||||||
|
...user
|
||||||
|
}));
|
||||||
|
|
||||||
return account;
|
return dispatch(setLocale(user.lang))
|
||||||
|
.then(() => account);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -95,3 +100,14 @@ export function activate(account) {
|
|||||||
payload: account
|
payload: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UPDATE_TOKEN = 'accounts:updateToken';
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
*/
|
||||||
|
export function updateToken(token) {
|
||||||
|
return {
|
||||||
|
type: UPDATE_TOKEN,
|
||||||
|
payload: token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ADD, REMOVE, ACTIVATE } from './actions';
|
import { ADD, REMOVE, ACTIVATE, UPDATE_TOKEN } from './actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {AccountsState}
|
* @typedef {AccountsState}
|
||||||
@ -14,7 +14,10 @@ import { ADD, REMOVE, ACTIVATE } from './actions';
|
|||||||
* @return {AccountsState}
|
* @return {AccountsState}
|
||||||
*/
|
*/
|
||||||
export default function accounts(
|
export default function accounts(
|
||||||
state,
|
state = {
|
||||||
|
active: null,
|
||||||
|
available: []
|
||||||
|
},
|
||||||
{type, payload = {}}
|
{type, payload = {}}
|
||||||
) {
|
) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -49,10 +52,20 @@ export default function accounts(
|
|||||||
available: state.available.filter((account) => account.id !== payload.id)
|
available: state.available.filter((account) => account.id !== payload.id)
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
case UPDATE_TOKEN:
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
throw new Error('payload must be a jwt token');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: null,
|
...state,
|
||||||
available: []
|
active: {
|
||||||
|
...state.active,
|
||||||
|
token: payload
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { routeActions } from 'react-router-redux';
|
import { routeActions } from 'react-router-redux';
|
||||||
|
|
||||||
import { updateUser, logout as logoutUser, acceptRules as userAcceptRules, authenticate } from 'components/user/actions';
|
import { updateUser, logout as logoutUser, acceptRules as userAcceptRules } from 'components/user/actions';
|
||||||
|
import { authenticate } from 'components/accounts/actions';
|
||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
import oauth from 'services/api/oauth';
|
import oauth from 'services/api/oauth';
|
||||||
import signup from 'services/api/signup';
|
import signup from 'services/api/signup';
|
||||||
@ -305,7 +306,10 @@ function needActivation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function authHandler(dispatch) {
|
function authHandler(dispatch) {
|
||||||
return (resp) => dispatch(authenticate(resp.access_token, resp.refresh_token));
|
return (resp) => dispatch(authenticate({
|
||||||
|
token: resp.access_token,
|
||||||
|
refreshToken: resp.refresh_token
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function validationErrorsHandler(dispatch, repeatUrl) {
|
function validationErrorsHandler(dispatch, repeatUrl) {
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
import i18n from 'services/i18n';
|
import i18n from 'services/i18n';
|
||||||
|
import captcha from 'services/captcha';
|
||||||
|
|
||||||
export const SET_LOCALE = 'SET_LOCALE';
|
export const SET_LOCALE = 'i18n:setLocale';
|
||||||
export function setLocale(locale) {
|
export function setLocale(locale) {
|
||||||
return (dispatch) => i18n.require(
|
return (dispatch) => i18n.require(
|
||||||
i18n.detectLanguage(locale)
|
i18n.detectLanguage(locale)
|
||||||
).then(({locale, messages}) => {
|
).then(({locale, messages}) => {
|
||||||
dispatch({
|
dispatch(_setLocale({locale, messages}));
|
||||||
type: SET_LOCALE,
|
|
||||||
payload: {
|
// TODO: probably should be moved from here, because it is a side effect
|
||||||
locale,
|
captcha.setLang(locale);
|
||||||
messages
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return locale;
|
return locale;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _setLocale({locale, messages}) {
|
||||||
|
return {
|
||||||
|
type: SET_LOCALE,
|
||||||
|
payload: {
|
||||||
|
locale,
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -33,10 +33,6 @@ export default class User {
|
|||||||
// frontend app specific attributes
|
// frontend app specific attributes
|
||||||
isGuest: true,
|
isGuest: true,
|
||||||
goal: null, // the goal with wich user entered site
|
goal: null, // the goal with wich user entered site
|
||||||
|
|
||||||
// TODO: the following does not belongs here. Move it later
|
|
||||||
token: '',
|
|
||||||
refreshToken: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = Object.keys(defaults).reduce((user, key) => {
|
const user = Object.keys(defaults).reduce((user, key) => {
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { routeActions } from 'react-router-redux';
|
import { routeActions } from 'react-router-redux';
|
||||||
|
|
||||||
import captcha from 'services/captcha';
|
|
||||||
import accounts from 'services/api/accounts';
|
import accounts from 'services/api/accounts';
|
||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
import { setLocale } from 'components/i18n/actions';
|
import { setLocale } from 'components/i18n/actions';
|
||||||
|
|
||||||
export const UPDATE = 'USER_UPDATE';
|
export const UPDATE = 'USER_UPDATE';
|
||||||
/**
|
/**
|
||||||
* @param {string|object} payload jwt token or user object
|
* Merge data into user's state
|
||||||
* @return {object} action definition
|
*
|
||||||
|
* @param {object} payload
|
||||||
|
* @return {object} - action definition
|
||||||
*/
|
*/
|
||||||
export function updateUser(payload) {
|
export function updateUser(payload) {
|
||||||
return {
|
return {
|
||||||
@ -23,23 +24,26 @@ export function changeLang(lang) {
|
|||||||
.then((lang) => {
|
.then((lang) => {
|
||||||
const {user: {isGuest, lang: oldLang}} = getState();
|
const {user: {isGuest, lang: oldLang}} = getState();
|
||||||
|
|
||||||
if (!isGuest && oldLang !== lang) {
|
if (oldLang !== lang) {
|
||||||
accounts.changeLang(lang);
|
!isGuest && accounts.changeLang(lang);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CHANGE_LANG,
|
||||||
|
payload: {
|
||||||
|
lang
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: probably should be moved from here, because it is side effect
|
|
||||||
captcha.setLang(lang);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: CHANGE_LANG,
|
|
||||||
payload: {
|
|
||||||
lang
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET = 'USER_SET';
|
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) {
|
export function setUser(payload) {
|
||||||
return {
|
return {
|
||||||
type: SET,
|
type: SET,
|
||||||
@ -72,7 +76,10 @@ export function fetchUserData() {
|
|||||||
return (dispatch) =>
|
return (dispatch) =>
|
||||||
accounts.current()
|
accounts.current()
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
dispatch(updateUser(resp));
|
dispatch(updateUser({
|
||||||
|
isGuest: false,
|
||||||
|
...resp
|
||||||
|
}));
|
||||||
|
|
||||||
return dispatch(changeLang(resp.lang));
|
return dispatch(changeLang(resp.lang));
|
||||||
});
|
});
|
||||||
@ -80,31 +87,11 @@ export function fetchUserData() {
|
|||||||
|
|
||||||
export function acceptRules() {
|
export function acceptRules() {
|
||||||
return (dispatch) =>
|
return (dispatch) =>
|
||||||
accounts.acceptRules()
|
accounts.acceptRules().then((resp) => {
|
||||||
.then((resp) => {
|
|
||||||
dispatch(updateUser({
|
dispatch(updateUser({
|
||||||
shouldAcceptRules: false
|
shouldAcceptRules: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return resp;
|
|
||||||
})
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
refreshToken = refreshToken || getState().user.refreshToken;
|
|
||||||
dispatch(updateUser({
|
|
||||||
token,
|
|
||||||
refreshToken
|
|
||||||
}));
|
|
||||||
|
|
||||||
return dispatch(fetchUserData()).then((resp) => {
|
|
||||||
dispatch(updateUser({
|
|
||||||
isGuest: false
|
|
||||||
}));
|
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { authenticate, changeLang } from 'components/user/actions';
|
import { changeLang } from 'components/user/actions';
|
||||||
|
import { authenticate } from 'components/accounts/actions';
|
||||||
|
|
||||||
import request from 'services/request';
|
import request from 'services/request';
|
||||||
import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';
|
import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';
|
||||||
@ -22,11 +23,11 @@ export function factory(store) {
|
|||||||
request.addMiddleware(bearerHeaderMiddleware(store));
|
request.addMiddleware(bearerHeaderMiddleware(store));
|
||||||
|
|
||||||
promise = new Promise((resolve, reject) => {
|
promise = new Promise((resolve, reject) => {
|
||||||
const {user} = store.getState();
|
const {user, accounts} = store.getState();
|
||||||
|
|
||||||
if (user.token) {
|
if (accounts.active || user.token) {
|
||||||
// authorizing user if it is possible
|
// authorizing user if it is possible
|
||||||
return store.dispatch(authenticate(user.token)).then(resolve, reject);
|
return store.dispatch(authenticate(accounts.active || user)).then(resolve, reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto-detect guests language
|
// auto-detect guests language
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
export default function bearerHeaderMiddleware({getState}) {
|
export default function bearerHeaderMiddleware({getState}) {
|
||||||
return {
|
return {
|
||||||
before(req) {
|
before(req) {
|
||||||
let {token} = getState().user;
|
const {user, accounts} = getState();
|
||||||
|
|
||||||
|
let {token} = accounts.active ? accounts.active : user;
|
||||||
|
|
||||||
if (req.options.token) {
|
if (req.options.token) {
|
||||||
token = req.options.token;
|
token = req.options.token;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
import {updateUser, logout} from '../actions';
|
import { updateToken } from 'components/accounts/actions';
|
||||||
|
import { logout } from '../actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures, that all user's requests have fresh access token
|
* Ensures, that all user's requests have fresh access token
|
||||||
@ -13,9 +14,21 @@ import {updateUser, logout} from '../actions';
|
|||||||
export default function refreshTokenMiddleware({dispatch, getState}) {
|
export default function refreshTokenMiddleware({dispatch, getState}) {
|
||||||
return {
|
return {
|
||||||
before(req) {
|
before(req) {
|
||||||
const {refreshToken, token} = getState().user;
|
const {user, accounts} = getState();
|
||||||
|
|
||||||
|
let refreshToken;
|
||||||
|
let token;
|
||||||
|
|
||||||
const isRefreshTokenRequest = req.url.includes('refresh-token');
|
const isRefreshTokenRequest = req.url.includes('refresh-token');
|
||||||
|
|
||||||
|
if (accounts.active) {
|
||||||
|
token = accounts.active.token;
|
||||||
|
refreshToken = accounts.active.refreshToken;
|
||||||
|
} else { // #legacy token
|
||||||
|
token = user.token;
|
||||||
|
refreshToken = user.refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
if (!token || isRefreshTokenRequest || req.options.autoRefreshToken === false) {
|
if (!token || isRefreshTokenRequest || req.options.autoRefreshToken === false) {
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@ -28,21 +41,24 @@ export default function refreshTokenMiddleware({dispatch, getState}) {
|
|||||||
return requestAccessToken(refreshToken, dispatch).then(() => req);
|
return requestAccessToken(refreshToken, dispatch).then(() => req);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dispatch(logout());
|
// console.error('Bad token', err); // TODO: it would be cool to log such things to backend
|
||||||
|
return dispatch(logout()).then(() => req);
|
||||||
}
|
}
|
||||||
|
|
||||||
return req;
|
return Promise.resolve(req);
|
||||||
},
|
},
|
||||||
|
|
||||||
catch(resp, req, restart) {
|
catch(resp, req, restart) {
|
||||||
if (resp && resp.status === 401 && req.options.autoRefreshToken !== false) {
|
if (resp && resp.status === 401 && req.options.autoRefreshToken !== false) {
|
||||||
const {refreshToken} = getState().user;
|
const {user, accounts} = getState();
|
||||||
|
const {refreshToken} = accounts.active ? accounts.active : user;
|
||||||
|
|
||||||
if (resp.message === 'Token expired' && refreshToken) {
|
if (resp.message === 'Token expired' && refreshToken) {
|
||||||
// request token and retry
|
// request token and retry
|
||||||
return requestAccessToken(refreshToken, dispatch).then(restart);
|
return requestAccessToken(refreshToken, dispatch).then(restart);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(logout());
|
return dispatch(logout()).then(() => Promise.reject(resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(resp);
|
return Promise.reject(resp);
|
||||||
@ -59,9 +75,7 @@ function requestAccessToken(refreshToken, dispatch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
.then(({token}) => dispatch(updateUser({
|
.then(({token}) => dispatch(updateToken(token)))
|
||||||
token
|
|
||||||
})))
|
|
||||||
.catch(() => dispatch(logout()));
|
.catch(() => dispatch(logout()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import request from 'services/request';
|
import request from 'services/request';
|
||||||
import accounts from 'services/api/accounts';
|
import accounts from 'services/api/accounts';
|
||||||
|
|
||||||
export default {
|
const authentication = {
|
||||||
login({
|
login({
|
||||||
login = '',
|
login = '',
|
||||||
password = '',
|
password = '',
|
||||||
@ -48,10 +48,27 @@ export default {
|
|||||||
* if it was refreshed
|
* if it was refreshed
|
||||||
*/
|
*/
|
||||||
validateToken({token, refreshToken}) {
|
validateToken({token, refreshToken}) {
|
||||||
// TODO: use refresh token to get fresh token. Dont forget, that it may be broken by refreshTokenMiddleware
|
return new Promise((resolve) => {
|
||||||
// TODO: cover with tests
|
if (typeof token !== 'string') {
|
||||||
return accounts.current({token, autoRefreshToken: false})
|
throw new Error('token must be a string');
|
||||||
.then(() => ({token, refreshToken}));
|
}
|
||||||
|
|
||||||
|
if (typeof refreshToken !== 'string') {
|
||||||
|
throw new Error('refreshToken must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.then(() => accounts.current({token, autoRefreshToken: false}))
|
||||||
|
.then(() => ({token, refreshToken}))
|
||||||
|
.catch((resp) => {
|
||||||
|
if (resp.message === 'Token expired') {
|
||||||
|
return authentication.requestToken(refreshToken)
|
||||||
|
.then(({token}) => ({token, refreshToken}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(resp);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,3 +87,5 @@ export default {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default authentication;
|
||||||
|
@ -17,7 +17,8 @@ export default function storeFactory() {
|
|||||||
thunk
|
thunk
|
||||||
);
|
);
|
||||||
const persistStateEnhancer = persistState([
|
const persistStateEnhancer = persistState([
|
||||||
'accounts'
|
'accounts',
|
||||||
|
'user'
|
||||||
], {key: 'redux-storage'});
|
], {key: 'redux-storage'});
|
||||||
|
|
||||||
/* global process: false */
|
/* global process: false */
|
||||||
|
@ -2,21 +2,23 @@ import expect from 'unexpected';
|
|||||||
|
|
||||||
import accounts from 'services/api/accounts';
|
import accounts from 'services/api/accounts';
|
||||||
import { authenticate, revoke, add, activate, remove, ADD, REMOVE, ACTIVATE } from 'components/accounts/actions';
|
import { authenticate, revoke, add, activate, remove, ADD, REMOVE, ACTIVATE } from 'components/accounts/actions';
|
||||||
|
import { SET_LOCALE } from 'components/i18n/actions';
|
||||||
|
|
||||||
import { updateUser, logout } from 'components/user/actions';
|
import { updateUser } from 'components/user/actions';
|
||||||
|
|
||||||
const account = {
|
const account = {
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'username',
|
username: 'username',
|
||||||
email: 'email@test.com',
|
email: 'email@test.com',
|
||||||
token: 'foo',
|
token: 'foo',
|
||||||
refreshToken: 'foo'
|
refreshToken: 'bar'
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'username',
|
username: 'username',
|
||||||
email: 'email@test.com',
|
email: 'email@test.com',
|
||||||
|
lang: 'be'
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Accounts actions', () => {
|
describe('Accounts actions', () => {
|
||||||
@ -24,10 +26,10 @@ describe('Accounts actions', () => {
|
|||||||
let getState;
|
let getState;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dispatch = sinon.spy(function dispatch(arg) {
|
dispatch = sinon.spy((arg) =>
|
||||||
return typeof arg === 'function' ? arg(dispatch, getState) : arg;
|
typeof arg === 'function' ? arg(dispatch, getState) : arg
|
||||||
}).named('dispatch');
|
).named('store.dispatch');
|
||||||
getState = sinon.stub().named('getState');
|
getState = sinon.stub().named('store.getState');
|
||||||
|
|
||||||
getState.returns({
|
getState.returns({
|
||||||
accounts: [],
|
accounts: [],
|
||||||
@ -43,13 +45,13 @@ describe('Accounts actions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('#authenticate()', () => {
|
describe('#authenticate()', () => {
|
||||||
it('should request user state using token', () => {
|
it('should request user state using token', () =>
|
||||||
authenticate(account)(dispatch);
|
authenticate(account)(dispatch).then(() =>
|
||||||
|
expect(accounts.current, 'to have a call satisfying', [
|
||||||
expect(accounts.current, 'to have a call satisfying', [
|
{token: account.token}
|
||||||
{token: account.token}
|
])
|
||||||
]);
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
it(`dispatches ${ADD} action`, () =>
|
it(`dispatches ${ADD} action`, () =>
|
||||||
authenticate(account)(dispatch).then(() =>
|
authenticate(account)(dispatch).then(() =>
|
||||||
@ -67,10 +69,18 @@ describe('Accounts actions', () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it(`dispatches ${SET_LOCALE} action`, () =>
|
||||||
|
authenticate(account)(dispatch).then(() =>
|
||||||
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
{type: SET_LOCALE, payload: {locale: 'be'}}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
it('should update user state', () =>
|
it('should update user state', () =>
|
||||||
authenticate(account)(dispatch).then(() =>
|
authenticate(account)(dispatch).then(() =>
|
||||||
expect(dispatch, 'to have a call satisfying', [
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
updateUser(user)
|
updateUser({...user, isGuest: false})
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -84,14 +94,9 @@ describe('Accounts actions', () => {
|
|||||||
it('rejects when bad auth data', () => {
|
it('rejects when bad auth data', () => {
|
||||||
accounts.current.returns(Promise.reject({}));
|
accounts.current.returns(Promise.reject({}));
|
||||||
|
|
||||||
const promise = authenticate(account)(dispatch);
|
return expect(authenticate(account)(dispatch), 'to be rejected').then(() =>
|
||||||
|
expect(dispatch, 'was not called')
|
||||||
expect(promise, 'to be rejected');
|
);
|
||||||
|
|
||||||
return promise.catch(() => {
|
|
||||||
expect(dispatch, 'was not called');
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,27 +113,42 @@ describe('Accounts actions', () => {
|
|||||||
const account2 = {...account, id: 2};
|
const account2 = {...account, id: 2};
|
||||||
|
|
||||||
getState.returns({
|
getState.returns({
|
||||||
accounts: [account2]
|
accounts: [account]
|
||||||
});
|
});
|
||||||
|
|
||||||
return revoke(account)(dispatch, getState).then(() =>
|
return revoke(account2)(dispatch, getState).then(() => {
|
||||||
expect(dispatch, 'to have calls satisfying', [
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
[remove(account)],
|
remove(account2)
|
||||||
[expect.it('to be a function')]
|
]);
|
||||||
// [authenticate(account2)] // TODO: this is not a plain action. How should we simplify its testing?
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
])
|
activate(account)
|
||||||
);
|
]);
|
||||||
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
updateUser({...user, isGuest: false})
|
||||||
|
]);
|
||||||
|
// expect(dispatch, 'to have calls satisfying', [
|
||||||
|
// [remove(account2)],
|
||||||
|
// [expect.it('to be a function')]
|
||||||
|
// // [authenticate(account2)] // TODO: this is not a plain action. How should we simplify its testing?
|
||||||
|
// ])
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should logout if no other accounts available', () => {
|
it('should logout if no other accounts available', () => {
|
||||||
revoke(account)(dispatch, getState)
|
revoke(account)(dispatch, getState).then(() => {
|
||||||
.then(() =>
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
expect(dispatch, 'to have calls satisfying', [
|
remove(account)
|
||||||
[remove(account)],
|
]);
|
||||||
[expect.it('to be a function')]
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
// [logout()] // TODO: this is not a plain action. How should we simplify its testing?
|
{payload: {isGuest: true}}
|
||||||
])
|
// updateUser({isGuest: true})
|
||||||
);
|
]);
|
||||||
|
// expect(dispatch, 'to have calls satisfying', [
|
||||||
|
// [remove(account)],
|
||||||
|
// [expect.it('to be a function')]
|
||||||
|
// // [logout()] // TODO: this is not a plain action. How should we simplify its testing?
|
||||||
|
// ])
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import expect from 'unexpected';
|
import expect from 'unexpected';
|
||||||
|
|
||||||
import accounts from 'components/accounts/reducer';
|
import accounts from 'components/accounts/reducer';
|
||||||
import { ADD, REMOVE, ACTIVATE } from 'components/accounts/actions';
|
import {
|
||||||
|
updateToken, add, remove, activate,
|
||||||
|
ADD, REMOVE, ACTIVATE, UPDATE_TOKEN
|
||||||
|
} from 'components/accounts/actions';
|
||||||
|
|
||||||
const account = {
|
const account = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -15,20 +18,21 @@ describe('Accounts reducer', () => {
|
|||||||
let initial;
|
let initial;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initial = accounts(null, {});
|
initial = accounts(undefined, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be empty', () => expect(accounts(null, {}), 'to equal', {
|
it('should be empty', () => expect(accounts(undefined, {}), 'to equal', {
|
||||||
active: null,
|
active: null,
|
||||||
available: []
|
available: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should return last state if unsupported action', () =>
|
||||||
|
expect(accounts({state: 'foo'}, {}), 'to equal', {state: 'foo'})
|
||||||
|
);
|
||||||
|
|
||||||
describe(ACTIVATE, () => {
|
describe(ACTIVATE, () => {
|
||||||
it('sets active account', () => {
|
it('sets active account', () => {
|
||||||
expect(accounts(initial, {
|
expect(accounts(initial, activate(account)), 'to satisfy', {
|
||||||
type: ACTIVATE,
|
|
||||||
payload: account
|
|
||||||
}), 'to satisfy', {
|
|
||||||
active: account
|
active: account
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -36,52 +40,49 @@ describe('Accounts reducer', () => {
|
|||||||
|
|
||||||
describe(ADD, () => {
|
describe(ADD, () => {
|
||||||
it('adds an account', () =>
|
it('adds an account', () =>
|
||||||
expect(accounts(initial, {
|
expect(accounts(initial, add(account)), 'to satisfy', {
|
||||||
type: ADD,
|
|
||||||
payload: account
|
|
||||||
}), 'to satisfy', {
|
|
||||||
available: [account]
|
available: [account]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should not add the same account twice', () =>
|
it('should not add the same account twice', () =>
|
||||||
expect(accounts({...initial, available: [account]}, {
|
expect(accounts({...initial, available: [account]}, add(account)), 'to satisfy', {
|
||||||
type: ADD,
|
|
||||||
payload: account
|
|
||||||
}), 'to satisfy', {
|
|
||||||
available: [account]
|
available: [account]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
it('throws, when account is invalid', () => {
|
it('throws, when account is invalid', () => {
|
||||||
expect(() => accounts(initial, {
|
expect(() => accounts(initial, add()),
|
||||||
type: ADD
|
'to throw', 'Invalid or empty payload passed for accounts.add');
|
||||||
}), 'to throw', 'Invalid or empty payload passed for accounts.add');
|
|
||||||
|
|
||||||
expect(() => accounts(initial, {
|
|
||||||
type: ADD,
|
|
||||||
payload: {}
|
|
||||||
}), 'to throw', 'Invalid or empty payload passed for accounts.add');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(REMOVE, () => {
|
describe(REMOVE, () => {
|
||||||
it('should remove an account', () =>
|
it('should remove an account', () =>
|
||||||
expect(accounts({...initial, available: [account]}, {
|
expect(accounts({...initial, available: [account]}, remove(account)),
|
||||||
type: REMOVE,
|
'to equal', initial)
|
||||||
payload: account
|
|
||||||
}), 'to equal', initial)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it('throws, when account is invalid', () => {
|
it('throws, when account is invalid', () => {
|
||||||
expect(() => accounts(initial, {
|
expect(() => accounts(initial, remove()),
|
||||||
type: REMOVE
|
'to throw', 'Invalid or empty payload passed for accounts.remove');
|
||||||
}), 'to throw', 'Invalid or empty payload passed for accounts.remove');
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(() => accounts(initial, {
|
describe(UPDATE_TOKEN, () => {
|
||||||
type: REMOVE,
|
it('should update token', () => {
|
||||||
payload: {}
|
const newToken = 'newToken';
|
||||||
}), 'to throw', 'Invalid or empty payload passed for accounts.remove');
|
|
||||||
|
expect(accounts(
|
||||||
|
{active: account, available: [account]},
|
||||||
|
updateToken(newToken)
|
||||||
|
), 'to satisfy', {
|
||||||
|
active: {
|
||||||
|
...account,
|
||||||
|
token: newToken
|
||||||
|
},
|
||||||
|
available: [account]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,8 +11,10 @@ import {
|
|||||||
|
|
||||||
|
|
||||||
describe('components/user/actions', () => {
|
describe('components/user/actions', () => {
|
||||||
const dispatch = sinon.stub().named('dispatch');
|
const getState = sinon.stub().named('store.getState');
|
||||||
const getState = sinon.stub().named('getState');
|
const dispatch = sinon.spy((arg) =>
|
||||||
|
typeof arg === 'function' ? arg(dispatch, getState) : arg
|
||||||
|
).named('store.dispatch');
|
||||||
|
|
||||||
const callThunk = function(fn, ...args) {
|
const callThunk = function(fn, ...args) {
|
||||||
const thunk = fn(...args);
|
const thunk = fn(...args);
|
||||||
|
@ -3,11 +3,21 @@ import expect from 'unexpected';
|
|||||||
import bearerHeaderMiddleware from 'components/user/middlewares/bearerHeaderMiddleware';
|
import bearerHeaderMiddleware from 'components/user/middlewares/bearerHeaderMiddleware';
|
||||||
|
|
||||||
describe('bearerHeaderMiddleware', () => {
|
describe('bearerHeaderMiddleware', () => {
|
||||||
|
const emptyState = {
|
||||||
|
user: {},
|
||||||
|
accounts: {
|
||||||
|
active: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('when token available', () => {
|
describe('when token available', () => {
|
||||||
const token = 'foo';
|
const token = 'foo';
|
||||||
const middleware = bearerHeaderMiddleware({
|
const middleware = bearerHeaderMiddleware({
|
||||||
getState: () => ({
|
getState: () => ({
|
||||||
user: {token}
|
...emptyState,
|
||||||
|
accounts: {
|
||||||
|
active: {token}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -20,9 +30,7 @@ describe('bearerHeaderMiddleware', () => {
|
|||||||
|
|
||||||
middleware.before(data);
|
middleware.before(data);
|
||||||
|
|
||||||
expect(data.options.headers, 'to satisfy', {
|
expectBearerHeader(data, token);
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('overrides user.token with options.token if available', () => {
|
it('overrides user.token with options.token if available', () => {
|
||||||
@ -36,16 +44,36 @@ describe('bearerHeaderMiddleware', () => {
|
|||||||
|
|
||||||
middleware.before(data);
|
middleware.before(data);
|
||||||
|
|
||||||
expect(data.options.headers, 'to satisfy', {
|
expectBearerHeader(data, tokenOverride);
|
||||||
Authorization: `Bearer ${tokenOverride}`
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when legacy token available', () => {
|
||||||
|
const token = 'foo';
|
||||||
|
const middleware = bearerHeaderMiddleware({
|
||||||
|
getState: () => ({
|
||||||
|
...emptyState,
|
||||||
|
user: {token}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set Authorization header', () => {
|
||||||
|
const data = {
|
||||||
|
options: {
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
middleware.before(data);
|
||||||
|
|
||||||
|
expectBearerHeader(data, token);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set Authorization header if no token', () => {
|
it('should not set Authorization header if no token', () => {
|
||||||
const middleware = bearerHeaderMiddleware({
|
const middleware = bearerHeaderMiddleware({
|
||||||
getState: () => ({
|
getState: () => ({
|
||||||
user: {}
|
...emptyState
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,4 +87,10 @@ describe('bearerHeaderMiddleware', () => {
|
|||||||
|
|
||||||
expect(data.options.headers.Authorization, 'to be undefined');
|
expect(data.options.headers.Authorization, 'to be undefined');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function expectBearerHeader(data, token) {
|
||||||
|
expect(data.options.headers, 'to satisfy', {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import expect from 'unexpected';
|
|||||||
import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware';
|
import refreshTokenMiddleware from 'components/user/middlewares/refreshTokenMiddleware';
|
||||||
|
|
||||||
import authentication from 'services/api/authentication';
|
import authentication from 'services/api/authentication';
|
||||||
|
import { updateToken } from 'components/accounts/actions';
|
||||||
|
|
||||||
const refreshToken = 'foo';
|
const refreshToken = 'foo';
|
||||||
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8';
|
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8';
|
||||||
@ -18,7 +19,9 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
sinon.stub(authentication, 'requestToken').named('authentication.requestToken');
|
sinon.stub(authentication, 'requestToken').named('authentication.requestToken');
|
||||||
|
|
||||||
getState = sinon.stub().named('store.getState');
|
getState = sinon.stub().named('store.getState');
|
||||||
dispatch = sinon.stub().named('store.dispatch');
|
dispatch = sinon.spy((arg) =>
|
||||||
|
typeof arg === 'function' ? arg(dispatch, getState) : arg
|
||||||
|
).named('store.dispatch');
|
||||||
|
|
||||||
middleware = refreshTokenMiddleware({getState, dispatch});
|
middleware = refreshTokenMiddleware({getState, dispatch});
|
||||||
});
|
});
|
||||||
@ -27,14 +30,21 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
authentication.requestToken.restore();
|
authentication.requestToken.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('must be till 2100 to test with validToken', () =>
|
||||||
|
expect(new Date().getFullYear(), 'to be less than', 2100)
|
||||||
|
);
|
||||||
|
|
||||||
describe('#before', () => {
|
describe('#before', () => {
|
||||||
describe('when token expired', () => {
|
describe('when token expired', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getState.returns({
|
getState.returns({
|
||||||
user: {
|
accounts: {
|
||||||
token: expiredToken,
|
active: {
|
||||||
refreshToken
|
token: expiredToken,
|
||||||
}
|
refreshToken
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,21 +86,94 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
expect(authentication.requestToken, 'was not called');
|
expect(authentication.requestToken, 'was not called');
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('should update user with new token'); // TODO: need a way to test, that action was called
|
it('should update user with new token', () => {
|
||||||
xit('should logout if invalid token'); // TODO: need a way to test, that action was called
|
const data = {
|
||||||
|
url: 'foo',
|
||||||
|
options: {
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
xit('should logout if token request failed', () => {
|
authentication.requestToken.returns(Promise.resolve({token: validToken}));
|
||||||
|
|
||||||
|
return middleware.before(data).then(() =>
|
||||||
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
updateToken(validToken)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should if token can not be parsed', () => {
|
||||||
|
getState.returns({
|
||||||
|
accounts: {
|
||||||
|
active: {
|
||||||
|
token: 'realy bad token',
|
||||||
|
refreshToken
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {url: 'foo', options: {}};
|
||||||
|
|
||||||
|
return expect(middleware.before(req), 'to be fulfilled with', req).then(() => {
|
||||||
|
expect(authentication.requestToken, 'was not called');
|
||||||
|
|
||||||
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
{payload: {isGuest: true}}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should logout if token request failed', () => {
|
||||||
authentication.requestToken.returns(Promise.reject());
|
authentication.requestToken.returns(Promise.reject());
|
||||||
|
|
||||||
return middleware.before({url: 'foo'}).then((resp) => {
|
return expect(middleware.before({url: 'foo', options: {}}), 'to be fulfilled').then(() =>
|
||||||
// TODO: need a way to test, that action was called
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
expect(dispatch, 'to have a call satisfying', logout);
|
{payload: {isGuest: true}}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when token expired legacy user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getState.returns({
|
||||||
|
accounts: {
|
||||||
|
active: null
|
||||||
|
},
|
||||||
|
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', () => {
|
it('should not be applied if no token', () => {
|
||||||
getState.returns({
|
getState.returns({
|
||||||
|
accounts: {
|
||||||
|
active: null
|
||||||
|
},
|
||||||
user: {}
|
user: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,7 +197,15 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
|
|
||||||
const badTokenReponse = {
|
const badTokenReponse = {
|
||||||
name: 'Unauthorized',
|
name: 'Unauthorized',
|
||||||
message: 'Token expired',
|
message: 'You are requesting with an invalid credential.',
|
||||||
|
code: 0,
|
||||||
|
status: 401,
|
||||||
|
type: 'yii\\web\\UnauthorizedHttpException'
|
||||||
|
};
|
||||||
|
|
||||||
|
const incorrectTokenReponse = {
|
||||||
|
name: 'Unauthorized',
|
||||||
|
message: 'Incorrect token',
|
||||||
code: 0,
|
code: 0,
|
||||||
status: 401,
|
status: 401,
|
||||||
type: 'yii\\web\\UnauthorizedHttpException'
|
type: 'yii\\web\\UnauthorizedHttpException'
|
||||||
@ -124,9 +215,10 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getState.returns({
|
getState.returns({
|
||||||
user: {
|
accounts: {
|
||||||
refreshToken
|
active: {refreshToken}
|
||||||
}
|
},
|
||||||
|
user: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
restart = sinon.stub().named('restart');
|
restart = sinon.stub().named('restart');
|
||||||
@ -143,12 +235,27 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
xit('should logout user if token cannot be refreshed', () => {
|
it('should logout user if invalid credential', () =>
|
||||||
// TODO: need a way to test, that action was called
|
expect(
|
||||||
return middleware.catch(badTokenReponse, {options: {}}, restart).then(() => {
|
middleware.catch(badTokenReponse, {options: {}}, restart),
|
||||||
// TODO
|
'to be rejected'
|
||||||
});
|
).then(() =>
|
||||||
});
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
{payload: {isGuest: true}}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should logout user if token is incorrect', () =>
|
||||||
|
expect(
|
||||||
|
middleware.catch(incorrectTokenReponse, {options: {}}, restart),
|
||||||
|
'to be rejected'
|
||||||
|
).then(() =>
|
||||||
|
expect(dispatch, 'to have a call satisfying', [
|
||||||
|
{payload: {isGuest: true}}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
it('should pass the request through if options.autoRefreshToken === false', () => {
|
it('should pass the request through if options.autoRefreshToken === false', () => {
|
||||||
const promise = middleware.catch(expiredResponse, {
|
const promise = middleware.catch(expiredResponse, {
|
||||||
@ -175,5 +282,25 @@ describe('refreshTokenMiddleware', () => {
|
|||||||
expect(authentication.requestToken, 'was not called');
|
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');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
91
tests/services/api/authentication.test.js
Normal file
91
tests/services/api/authentication.test.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import expect from 'unexpected';
|
||||||
|
|
||||||
|
import authentication from 'services/api/authentication';
|
||||||
|
import accounts from 'services/api/accounts';
|
||||||
|
|
||||||
|
describe('authentication api', () => {
|
||||||
|
describe('#validateToken()', () => {
|
||||||
|
const validTokens = {token: 'foo', refreshToken: 'bar'};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(accounts, 'current');
|
||||||
|
|
||||||
|
accounts.current.returns(Promise.resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
accounts.current.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should request accounts.current', () =>
|
||||||
|
expect(authentication.validateToken(validTokens), 'to be fulfilled')
|
||||||
|
.then(() => {
|
||||||
|
expect(accounts.current, 'to have a call satisfying', [
|
||||||
|
{token: 'foo', autoRefreshToken: false}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should resolve with both tokens', () =>
|
||||||
|
expect(authentication.validateToken(validTokens), 'to be fulfilled with', validTokens)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects if token has a bad type', () =>
|
||||||
|
expect(authentication.validateToken({token: {}}),
|
||||||
|
'to be rejected with', 'token must be a string'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects if refreshToken has a bad type', () =>
|
||||||
|
expect(authentication.validateToken({token: 'foo', refreshToken: {}}),
|
||||||
|
'to be rejected with', 'refreshToken must be a string'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects if accounts.current request is unexpectedly failed', () => {
|
||||||
|
const error = 'Something wrong';
|
||||||
|
accounts.current.returns(Promise.reject(error));
|
||||||
|
|
||||||
|
return expect(authentication.validateToken(validTokens),
|
||||||
|
'to be rejected with', error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when token is expired', () => {
|
||||||
|
const expiredResponse = {
|
||||||
|
name: 'Unauthorized',
|
||||||
|
message: 'Token expired',
|
||||||
|
code: 0,
|
||||||
|
status: 401,
|
||||||
|
type: 'yii\\web\\UnauthorizedHttpException'
|
||||||
|
};
|
||||||
|
const newToken = 'baz';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(authentication, 'requestToken');
|
||||||
|
|
||||||
|
accounts.current.returns(Promise.reject(expiredResponse));
|
||||||
|
authentication.requestToken.returns(Promise.resolve({token: newToken}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
authentication.requestToken.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves with new token', () =>
|
||||||
|
expect(authentication.validateToken(validTokens),
|
||||||
|
'to be fulfilled with', {...validTokens, token: newToken}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects if token request failed', () => {
|
||||||
|
const error = 'Something wrong';
|
||||||
|
authentication.requestToken.returns(Promise.reject(error));
|
||||||
|
|
||||||
|
return expect(authentication.validateToken(validTokens),
|
||||||
|
'to be rejected with', error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user