#48: initial logic for multy-accounts actions

This commit is contained in:
SleepWalker
2016-10-30 14:12:49 +02:00
parent 200a1f339f
commit 7dd58acede
13 changed files with 572 additions and 116 deletions

View File

@@ -0,0 +1,134 @@
import expect from 'unexpected';
import accounts from 'services/api/accounts';
import { authenticate, revoke, add, activate, remove, ADD, REMOVE, ACTIVATE } from 'components/accounts/actions';
import { updateUser, logout } from 'components/user/actions';
const account = {
id: 1,
username: 'username',
email: 'email@test.com',
token: 'foo',
refreshToken: 'foo'
};
const user = {
id: 1,
username: 'username',
email: 'email@test.com',
};
describe('Accounts actions', () => {
let dispatch;
let getState;
beforeEach(() => {
dispatch = sinon.spy(function dispatch(arg) {
return typeof arg === 'function' ? arg(dispatch, getState) : arg;
}).named('dispatch');
getState = sinon.stub().named('getState');
getState.returns({
accounts: [],
user: {}
});
sinon.stub(accounts, 'current').named('accounts.current');
accounts.current.returns(Promise.resolve(user));
});
afterEach(() => {
accounts.current.restore();
});
describe('#authenticate()', () => {
it('should request user state using token', () => {
authenticate(account)(dispatch);
expect(accounts.current, 'to have a call satisfying', [
{token: account.token}
]);
});
it(`dispatches ${ADD} action`, () =>
authenticate(account)(dispatch).then(() =>
expect(dispatch, 'to have a call satisfying', [
add(account)
])
)
);
it(`dispatches ${ACTIVATE} action`, () =>
authenticate(account)(dispatch).then(() =>
expect(dispatch, 'to have a call satisfying', [
activate(account)
])
)
);
it('should update user state', () =>
authenticate(account)(dispatch).then(() =>
expect(dispatch, 'to have a call satisfying', [
updateUser(user)
])
)
);
it('resolves with account', () =>
authenticate(account)(dispatch).then((resp) =>
expect(resp, 'to equal', account)
)
);
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();
});
});
});
describe('#revoke()', () => {
it(`should dispatch ${REMOVE} action`, () => {
revoke(account)(dispatch, getState);
expect(dispatch, 'to have a call satisfying', [
remove(account)
]);
});
it('should switch next account if available', () => {
const account2 = {...account, id: 2};
getState.returns({
accounts: [account2]
});
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?
])
);
});
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?
])
);
});
});
});

View File

@@ -0,0 +1,87 @@
import expect from 'unexpected';
import accounts from 'components/accounts/reducer';
import { ADD, REMOVE, ACTIVATE } from 'components/accounts/actions';
const account = {
id: 1,
username: 'username',
email: 'email@test.com',
token: 'foo',
refreshToken: 'foo'
};
describe('Accounts reducer', () => {
let initial;
beforeEach(() => {
initial = accounts(null, {});
});
it('should be empty', () => expect(accounts(null, {}), 'to equal', {
active: null,
available: []
}));
describe(ACTIVATE, () => {
it('sets active account', () => {
expect(accounts(initial, {
type: ACTIVATE,
payload: account
}), 'to satisfy', {
active: account
});
});
});
describe(ADD, () => {
it('adds an account', () =>
expect(accounts(initial, {
type: ADD,
payload: account
}), 'to satisfy', {
available: [account]
})
);
it('should not add the same account twice', () =>
expect(accounts({...initial, available: [account]}, {
type: ADD,
payload: 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');
});
});
describe(REMOVE, () => {
it('should remove an account', () =>
expect(accounts({...initial, available: [account]}, {
type: REMOVE,
payload: 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, {
type: REMOVE,
payload: {}
}), 'to throw', 'Invalid or empty payload passed for accounts.remove');
});
});
});

View File

@@ -3,7 +3,7 @@ import expect from 'unexpected';
import bearerHeaderMiddleware from 'components/user/middlewares/bearerHeaderMiddleware';
describe('bearerHeaderMiddleware', () => {
it('should set Authorization header', () => {
describe('when token available', () => {
const token = 'foo';
const middleware = bearerHeaderMiddleware({
getState: () => ({
@@ -11,16 +11,34 @@ describe('bearerHeaderMiddleware', () => {
})
});
const data = {
options: {
headers: {}
}
};
it('should set Authorization header', () => {
const data = {
options: {
headers: {}
}
};
middleware.before(data);
middleware.before(data);
expect(data.options.headers, 'to satisfy', {
Authorization: `Bearer ${token}`
expect(data.options.headers, 'to satisfy', {
Authorization: `Bearer ${token}`
});
});
it('overrides user.token with options.token if available', () => {
const tokenOverride = 'tokenOverride';
const data = {
options: {
headers: {},
token: tokenOverride
}
};
middleware.before(data);
expect(data.options.headers, 'to satisfy', {
Authorization: `Bearer ${tokenOverride}`
});
});
});

View File

@@ -28,29 +28,64 @@ describe('refreshTokenMiddleware', () => {
});
describe('#before', () => {
it('should request new token', () => {
getState.returns({
user: {
token: expiredToken,
refreshToken
}
describe('when token expired', () => {
beforeEach(() => {
getState.returns({
user: {
token: expiredToken,
refreshToken
}
});
});
const data = {
url: 'foo',
options: {
headers: {}
}
};
it('should request new token', () => {
const data = {
url: 'foo',
options: {
headers: {}
}
};
authentication.requestToken.returns(Promise.resolve({token: validToken}));
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 apply to refresh-token request', () => {
const data = {url: '/refresh-token'};
const resp = middleware.before(data);
return middleware.before(data).then((resp) => {
expect(resp, 'to satisfy', data);
expect(authentication.requestToken, 'to have a call satisfying', [
refreshToken
]);
expect(authentication.requestToken, 'was not called');
});
it('should not apply if options.autoRefreshToken === false', () => {
const data = {
url: 'foo',
options: {autoRefreshToken: false}
};
middleware.before(data);
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
xit('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);
});
});
});
@@ -66,74 +101,78 @@ describe('refreshTokenMiddleware', () => {
expect(authentication.requestToken, 'was not called');
});
it('should not apply to refresh-token request', () => {
getState.returns({
user: {}
});
const data = {url: '/refresh-token'};
const resp = middleware.before(data);
expect(resp, 'to satisfy', data);
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
xit('should logout if token request failed', () => {
getState.returns({
user: {
token: expiredToken,
refreshToken
}
});
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);
});
});
});
describe('#catch', () => {
it('should request new token', () => {
const expiredResponse = {
name: 'Unauthorized',
message: 'Token expired',
code: 0,
status: 401,
type: 'yii\\web\\UnauthorizedHttpException'
};
const badTokenReponse = {
name: 'Unauthorized',
message: 'Token expired',
code: 0,
status: 401,
type: 'yii\\web\\UnauthorizedHttpException'
};
let restart;
beforeEach(() => {
getState.returns({
user: {
refreshToken
}
});
const restart = sinon.stub().named('restart');
restart = sinon.stub().named('restart');
authentication.requestToken.returns(Promise.resolve({token: validToken}));
});
return middleware.catch({
status: 401,
message: 'Token expired'
}, restart).then(() => {
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');
})
);
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
});
});
xit('should logout user if token cannot be refreshed'); // TODO: need a way to test, that action was called
it('should pass the request through if options.autoRefreshToken === false', () => {
const promise = middleware.catch(expiredResponse, {
options: {
autoRefreshToken: false
}
}, restart);
return expect(promise, 'to be rejected with', expiredResponse).then(() => {
expect(restart, 'was not called');
expect(authentication.requestToken, 'was not called');
});
});
it('should pass the rest of failed requests through', () => {
const resp = {};
const promise = middleware.catch(resp);
const promise = middleware.catch(resp, {
options: {}
}, restart);
expect(promise, 'to be rejected');
return promise.catch((actual) => {
expect(actual, 'to be', resp);
return expect(promise, 'to be rejected with', resp).then(() => {
expect(restart, 'was not called');
expect(authentication.requestToken, 'was not called');
});
});
});