mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
#48: integrate accounts with app
This commit is contained in:
@@ -2,21 +2,23 @@ import expect from 'unexpected';
|
||||
|
||||
import accounts from 'services/api/accounts';
|
||||
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 = {
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
token: 'foo',
|
||||
refreshToken: 'foo'
|
||||
refreshToken: 'bar'
|
||||
};
|
||||
|
||||
const user = {
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
lang: 'be'
|
||||
};
|
||||
|
||||
describe('Accounts actions', () => {
|
||||
@@ -24,10 +26,10 @@ describe('Accounts actions', () => {
|
||||
let getState;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = sinon.spy(function dispatch(arg) {
|
||||
return typeof arg === 'function' ? arg(dispatch, getState) : arg;
|
||||
}).named('dispatch');
|
||||
getState = sinon.stub().named('getState');
|
||||
dispatch = sinon.spy((arg) =>
|
||||
typeof arg === 'function' ? arg(dispatch, getState) : arg
|
||||
).named('store.dispatch');
|
||||
getState = sinon.stub().named('store.getState');
|
||||
|
||||
getState.returns({
|
||||
accounts: [],
|
||||
@@ -43,13 +45,13 @@ describe('Accounts actions', () => {
|
||||
});
|
||||
|
||||
describe('#authenticate()', () => {
|
||||
it('should request user state using token', () => {
|
||||
authenticate(account)(dispatch);
|
||||
|
||||
expect(accounts.current, 'to have a call satisfying', [
|
||||
{token: account.token}
|
||||
]);
|
||||
});
|
||||
it('should request user state using token', () =>
|
||||
authenticate(account)(dispatch).then(() =>
|
||||
expect(accounts.current, 'to have a call satisfying', [
|
||||
{token: account.token}
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
it(`dispatches ${ADD} action`, () =>
|
||||
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', () =>
|
||||
authenticate(account)(dispatch).then(() =>
|
||||
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', () => {
|
||||
accounts.current.returns(Promise.reject({}));
|
||||
|
||||
const promise = authenticate(account)(dispatch);
|
||||
|
||||
expect(promise, 'to be rejected');
|
||||
|
||||
return promise.catch(() => {
|
||||
expect(dispatch, 'was not called');
|
||||
return Promise.resolve();
|
||||
});
|
||||
return expect(authenticate(account)(dispatch), 'to be rejected').then(() =>
|
||||
expect(dispatch, 'was not called')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,27 +113,42 @@ describe('Accounts actions', () => {
|
||||
const account2 = {...account, id: 2};
|
||||
|
||||
getState.returns({
|
||||
accounts: [account2]
|
||||
accounts: [account]
|
||||
});
|
||||
|
||||
return revoke(account)(dispatch, getState).then(() =>
|
||||
expect(dispatch, 'to have calls satisfying', [
|
||||
[remove(account)],
|
||||
[expect.it('to be a function')]
|
||||
// [authenticate(account2)] // TODO: this is not a plain action. How should we simplify its testing?
|
||||
])
|
||||
);
|
||||
return revoke(account2)(dispatch, getState).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
remove(account2)
|
||||
]);
|
||||
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', () => {
|
||||
revoke(account)(dispatch, getState)
|
||||
.then(() =>
|
||||
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?
|
||||
])
|
||||
);
|
||||
revoke(account)(dispatch, getState).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
remove(account)
|
||||
]);
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
{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 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 = {
|
||||
id: 1,
|
||||
@@ -15,20 +18,21 @@ describe('Accounts reducer', () => {
|
||||
let initial;
|
||||
|
||||
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,
|
||||
available: []
|
||||
}));
|
||||
|
||||
it('should return last state if unsupported action', () =>
|
||||
expect(accounts({state: 'foo'}, {}), 'to equal', {state: 'foo'})
|
||||
);
|
||||
|
||||
describe(ACTIVATE, () => {
|
||||
it('sets active account', () => {
|
||||
expect(accounts(initial, {
|
||||
type: ACTIVATE,
|
||||
payload: account
|
||||
}), 'to satisfy', {
|
||||
expect(accounts(initial, activate(account)), 'to satisfy', {
|
||||
active: account
|
||||
});
|
||||
});
|
||||
@@ -36,52 +40,49 @@ describe('Accounts reducer', () => {
|
||||
|
||||
describe(ADD, () => {
|
||||
it('adds an account', () =>
|
||||
expect(accounts(initial, {
|
||||
type: ADD,
|
||||
payload: account
|
||||
}), 'to satisfy', {
|
||||
expect(accounts(initial, add(account)), 'to satisfy', {
|
||||
available: [account]
|
||||
})
|
||||
);
|
||||
|
||||
it('should not add the same account twice', () =>
|
||||
expect(accounts({...initial, available: [account]}, {
|
||||
type: ADD,
|
||||
payload: account
|
||||
}), 'to satisfy', {
|
||||
expect(accounts({...initial, available: [account]}, add(account)), 'to satisfy', {
|
||||
available: [account]
|
||||
})
|
||||
);
|
||||
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(() => accounts(initial, {
|
||||
type: 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');
|
||||
expect(() => accounts(initial, add()),
|
||||
'to throw', 'Invalid or empty payload passed for accounts.add');
|
||||
});
|
||||
});
|
||||
|
||||
describe(REMOVE, () => {
|
||||
it('should remove an account', () =>
|
||||
expect(accounts({...initial, available: [account]}, {
|
||||
type: REMOVE,
|
||||
payload: account
|
||||
}), 'to equal', initial)
|
||||
expect(accounts({...initial, available: [account]}, remove(account)),
|
||||
'to equal', initial)
|
||||
);
|
||||
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(() => accounts(initial, {
|
||||
type: REMOVE
|
||||
}), 'to throw', 'Invalid or empty payload passed for accounts.remove');
|
||||
expect(() => accounts(initial, remove()),
|
||||
'to throw', 'Invalid or empty payload passed for accounts.remove');
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => accounts(initial, {
|
||||
type: REMOVE,
|
||||
payload: {}
|
||||
}), 'to throw', 'Invalid or empty payload passed for accounts.remove');
|
||||
describe(UPDATE_TOKEN, () => {
|
||||
it('should update token', () => {
|
||||
const newToken = 'newToken';
|
||||
|
||||
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', () => {
|
||||
const dispatch = sinon.stub().named('dispatch');
|
||||
const getState = sinon.stub().named('getState');
|
||||
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);
|
||||
|
@@ -3,11 +3,21 @@ import expect from 'unexpected';
|
||||
import bearerHeaderMiddleware from 'components/user/middlewares/bearerHeaderMiddleware';
|
||||
|
||||
describe('bearerHeaderMiddleware', () => {
|
||||
const emptyState = {
|
||||
user: {},
|
||||
accounts: {
|
||||
active: null
|
||||
}
|
||||
};
|
||||
|
||||
describe('when token available', () => {
|
||||
const token = 'foo';
|
||||
const middleware = bearerHeaderMiddleware({
|
||||
getState: () => ({
|
||||
user: {token}
|
||||
...emptyState,
|
||||
accounts: {
|
||||
active: {token}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -20,9 +30,7 @@ describe('bearerHeaderMiddleware', () => {
|
||||
|
||||
middleware.before(data);
|
||||
|
||||
expect(data.options.headers, 'to satisfy', {
|
||||
Authorization: `Bearer ${token}`
|
||||
});
|
||||
expectBearerHeader(data, token);
|
||||
});
|
||||
|
||||
it('overrides user.token with options.token if available', () => {
|
||||
@@ -36,16 +44,36 @@ describe('bearerHeaderMiddleware', () => {
|
||||
|
||||
middleware.before(data);
|
||||
|
||||
expect(data.options.headers, 'to satisfy', {
|
||||
Authorization: `Bearer ${tokenOverride}`
|
||||
});
|
||||
expectBearerHeader(data, 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', () => {
|
||||
const middleware = bearerHeaderMiddleware({
|
||||
getState: () => ({
|
||||
user: {}
|
||||
...emptyState
|
||||
})
|
||||
});
|
||||
|
||||
@@ -59,4 +87,10 @@ describe('bearerHeaderMiddleware', () => {
|
||||
|
||||
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 authentication from 'services/api/authentication';
|
||||
import { updateToken } from 'components/accounts/actions';
|
||||
|
||||
const refreshToken = 'foo';
|
||||
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8';
|
||||
@@ -18,7 +19,9 @@ describe('refreshTokenMiddleware', () => {
|
||||
sinon.stub(authentication, 'requestToken').named('authentication.requestToken');
|
||||
|
||||
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});
|
||||
});
|
||||
@@ -27,14 +30,21 @@ describe('refreshTokenMiddleware', () => {
|
||||
authentication.requestToken.restore();
|
||||
});
|
||||
|
||||
it('must be till 2100 to test with validToken', () =>
|
||||
expect(new Date().getFullYear(), 'to be less than', 2100)
|
||||
);
|
||||
|
||||
describe('#before', () => {
|
||||
describe('when token expired', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
user: {
|
||||
token: expiredToken,
|
||||
refreshToken
|
||||
}
|
||||
accounts: {
|
||||
active: {
|
||||
token: expiredToken,
|
||||
refreshToken
|
||||
}
|
||||
},
|
||||
user: {}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,21 +86,94 @@ describe('refreshTokenMiddleware', () => {
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
});
|
||||
|
||||
xit('should update user with new token'); // TODO: need a way to test, that action was called
|
||||
xit('should logout if invalid token'); // TODO: need a way to test, that action was called
|
||||
it('should update user with new token', () => {
|
||||
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());
|
||||
|
||||
return middleware.before({url: 'foo'}).then((resp) => {
|
||||
// TODO: need a way to test, that action was called
|
||||
expect(dispatch, 'to have a call satisfying', logout);
|
||||
return expect(middleware.before({url: 'foo', options: {}}), 'to be fulfilled').then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
{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', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: null
|
||||
},
|
||||
user: {}
|
||||
});
|
||||
|
||||
@@ -114,7 +197,15 @@ describe('refreshTokenMiddleware', () => {
|
||||
|
||||
const badTokenReponse = {
|
||||
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,
|
||||
status: 401,
|
||||
type: 'yii\\web\\UnauthorizedHttpException'
|
||||
@@ -124,9 +215,10 @@ describe('refreshTokenMiddleware', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
user: {
|
||||
refreshToken
|
||||
}
|
||||
accounts: {
|
||||
active: {refreshToken}
|
||||
},
|
||||
user: {}
|
||||
});
|
||||
|
||||
restart = sinon.stub().named('restart');
|
||||
@@ -143,12 +235,27 @@ describe('refreshTokenMiddleware', () => {
|
||||
})
|
||||
);
|
||||
|
||||
xit('should logout user if token cannot be refreshed', () => {
|
||||
// TODO: need a way to test, that action was called
|
||||
return middleware.catch(badTokenReponse, {options: {}}, restart).then(() => {
|
||||
// TODO
|
||||
});
|
||||
});
|
||||
it('should logout user if invalid credential', () =>
|
||||
expect(
|
||||
middleware.catch(badTokenReponse, {options: {}}, restart),
|
||||
'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', () => {
|
||||
const promise = middleware.catch(expiredResponse, {
|
||||
@@ -175,5 +282,25 @@ 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');
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user