accounts-frontend/packages/app/components/accounts/actions.test.ts

439 lines
16 KiB
TypeScript
Raw Permalink Normal View History

import expect from 'app/test/unexpected';
import sinon from 'sinon';
import { browserHistory } from 'app/services/history';
import { InternalServerError } from 'app/services/request';
import { sessionStorage } from 'app/services/localStorage';
import * as authentication from 'app/services/api/authentication';
2020-05-24 04:38:24 +05:30
import { authenticate, revoke, logoutAll, logoutStrangers } from 'app/components/accounts/actions';
import { add, activate, remove, reset } from 'app/components/accounts/actions/pure-actions';
import { updateUser, setUser } from 'app/components/user/actions';
import { setLogin } from 'app/components/auth/actions';
import { Dispatch, State as RootState } from 'app/types';
2019-12-07 16:58:52 +05:30
import { Account } from './reducer';
import { User } from 'app/components/user';
2020-06-04 23:33:24 +05:30
jest.mock('app/i18n', () => ({
en: {
code: 'en',
name: 'English',
englishName: 'English',
progress: 100,
isReleased: true,
},
be: {
code: 'be',
name: 'Беларуская',
englishName: 'Belarusian',
progress: 97,
isReleased: true,
},
}));
2020-05-24 04:38:24 +05:30
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I';
const legacyToken = 'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOjF9.cRF-sQNrwWQ94xCb3vWioVdjxAZeefEE7GMGwh7708o';
const account: Account = {
2020-05-24 04:38:24 +05:30
id: 1,
username: 'username',
email: 'email@test.com',
token,
refreshToken: 'bar',
isDeleted: false,
};
const user: Partial<User> = {
2020-05-24 04:38:24 +05:30
id: 1,
username: 'username',
email: 'email@test.com',
lang: 'be',
isDeleted: false,
};
describe('components/accounts/actions', () => {
2020-05-24 04:38:24 +05:30
let dispatch: Dispatch;
let getState: () => RootState;
2020-05-24 04:38:24 +05:30
beforeEach(() => {
dispatch = sinon
.spy((arg) => (typeof arg === 'function' ? arg(dispatch, getState) : arg))
.named('store.dispatch');
getState = sinon.stub().named('store.getState');
2019-12-07 16:58:52 +05:30
(getState as any).returns({
2020-05-24 04:38:24 +05:30
accounts: {
available: [],
active: null,
},
auth: {
credentials: {},
},
2020-05-24 04:38:24 +05:30
user: {},
});
2020-05-24 04:38:24 +05:30
sinon.stub(authentication, 'validateToken').named('authentication.validateToken');
sinon.stub(browserHistory, 'push').named('browserHistory.push');
sinon.stub(authentication, 'logout').named('authentication.logout');
2020-05-24 04:38:24 +05:30
(authentication.logout as any).returns(Promise.resolve());
2019-12-07 16:58:52 +05:30
(authentication.validateToken as any).returns(
2020-05-24 04:38:24 +05:30
Promise.resolve({
token: account.token,
refreshToken: account.refreshToken,
user,
}),
2019-12-07 16:58:52 +05:30
);
2020-05-24 04:38:24 +05:30
});
2020-05-24 04:38:24 +05:30
afterEach(() => {
(authentication.validateToken as any).restore();
(authentication.logout as any).restore();
(browserHistory.push as any).restore();
});
2020-05-24 04:38:24 +05:30
describe('#authenticate()', () => {
it('should request user state using token', () =>
authenticate(account)(dispatch, getState, undefined).then(() =>
expect(authentication.validateToken, 'to have a call satisfying', [
account.id,
account.token,
account.refreshToken,
]),
));
it('should request user by extracting id from token', () =>
authenticate({ token } as Account)(dispatch, getState, undefined).then(() =>
expect(authentication.validateToken, 'to have a call satisfying', [1, token, undefined]),
));
it('should request user by extracting id from legacy token', () =>
authenticate({ token: legacyToken } as Account)(dispatch, getState, undefined).then(() =>
expect(authentication.validateToken, 'to have a call satisfying', [1, legacyToken, undefined]),
));
it(`dispatches accounts:add action`, () =>
authenticate(account)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [add(account)]),
));
it(`dispatches accounts:activate action`, () =>
authenticate(account)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [activate(account)]),
));
it(`dispatches i18n:setLocale action`, () =>
authenticate(account)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [{ type: 'i18n:setLocale', payload: { locale: 'be' } }]),
));
it('should update user state', () =>
authenticate(account)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [updateUser({ ...user, isGuest: false })]),
));
it('resolves with account', () =>
authenticate(account)(dispatch, getState, undefined).then((resp) => expect(resp, 'to equal', account)));
it('rejects when bad auth data', () => {
(authentication.validateToken as any).returns(Promise.reject({}));
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected').then(() => {
expect(dispatch, 'to have a call satisfying', [setLogin(account.email)]);
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
});
});
2020-05-24 04:38:24 +05:30
it('rejects when 5xx without logouting', () => {
const resp = new InternalServerError('500', { status: 500 });
2020-05-24 04:38:24 +05:30
(authentication.validateToken as any).rejects(resp);
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected with', resp).then(() =>
expect(dispatch, 'to have no calls satisfying', [{ payload: { isGuest: true } }]),
);
});
2020-05-24 04:38:24 +05:30
it('marks user as stranger, if there is no refreshToken', () => {
const expectedKey = `stranger${account.id}`;
(authentication.validateToken as any).resolves({
token: account.token,
user,
});
2020-05-24 04:38:24 +05:30
sessionStorage.removeItem(expectedKey);
2020-05-24 04:38:24 +05:30
return authenticate(account)(dispatch, getState, undefined).then(() => {
expect(sessionStorage.getItem(expectedKey), 'not to be null');
sessionStorage.removeItem(expectedKey);
});
});
describe('when user authenticated during oauth', () => {
beforeEach(() => {
(getState as any).returns({
accounts: {
available: [],
active: null,
},
user: {},
auth: {
oauth: {
clientId: 'ely.by',
prompt: [],
},
},
});
});
});
2020-05-24 04:38:24 +05:30
describe('when one account available', () => {
beforeEach(() => {
(getState as any).returns({
accounts: {
active: account.id,
available: [account],
},
auth: {
credentials: {},
},
user,
});
});
it('should activate account before auth api call', () => {
(authentication.validateToken as any).returns(Promise.reject({ error: 'foo' }));
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected with', {
error: 'foo',
}).then(() => expect(dispatch, 'to have a call satisfying', [activate(account)]));
});
});
});
2020-05-24 04:38:24 +05:30
describe('#revoke()', () => {
describe('when one account available', () => {
beforeEach(() => {
(getState as any).returns({
accounts: {
active: account.id,
available: [account],
},
auth: {
credentials: {},
},
user,
});
});
it('should dispatch reset action', () =>
revoke(account)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [reset()]),
));
it('should call logout api method in background', () =>
revoke(account)(dispatch, getState, undefined).then(() =>
expect(authentication.logout, 'to have a call satisfying', [account.token]),
));
it('should update user state', () =>
revoke(account)(dispatch, getState, undefined).then(
() => expect(dispatch, 'to have a call satisfying', [setUser({ 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?
// ])
));
});
2020-05-24 04:38:24 +05:30
describe('when multiple accounts available', () => {
const account2 = { ...account, id: 2 };
beforeEach(() => {
(getState as any).returns({
accounts: {
active: account2.id,
available: [account, account2],
},
user,
});
});
it('should switch to the next account', () =>
revoke(account2)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [activate(account)]),
));
it('should remove current account', () =>
revoke(account2)(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [remove(account2)]),
));
it('should call logout api method in background', () =>
revoke(account2)(dispatch, getState, undefined).then(() =>
expect(authentication.logout, 'to have a call satisfying', [account2.token]),
));
});
});
2020-05-24 04:38:24 +05:30
describe('#logoutAll()', () => {
const account2 = { ...account, id: 2 };
beforeEach(() => {
(getState as any).returns({
accounts: {
active: account2.id,
available: [account, account2],
},
auth: {
credentials: {},
},
user,
});
});
2020-05-24 04:38:24 +05:30
it('should call logout api method for each account', () => {
logoutAll()(dispatch, getState, undefined);
2020-05-24 04:38:24 +05:30
expect(authentication.logout, 'to have calls satisfying', [[account.token], [account2.token]]);
});
2020-05-24 04:38:24 +05:30
it('should dispatch reset', () => {
logoutAll()(dispatch, getState, undefined);
2020-05-24 04:38:24 +05:30
expect(dispatch, 'to have a call satisfying', [reset()]);
});
2020-05-24 04:38:24 +05:30
it('should redirect to /login', () =>
logoutAll()(dispatch, getState, undefined).then(() => {
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
}));
it('should change user to guest', () =>
logoutAll()(dispatch, getState, undefined).then(() => {
expect(dispatch, 'to have a call satisfying', [
setUser({
lang: user.lang,
isGuest: true,
}),
]);
}));
});
2020-05-24 04:38:24 +05:30
describe('#logoutStrangers', () => {
const foreignAccount = {
...account,
id: 2,
refreshToken: null,
};
const foreignAccount2 = {
...foreignAccount,
id: 3,
};
beforeEach(() => {
(getState as any).returns({
accounts: {
active: foreignAccount.id,
available: [account, foreignAccount, foreignAccount2],
},
user,
});
});
2020-05-24 04:38:24 +05:30
it('should remove stranger accounts', () => {
logoutStrangers()(dispatch, getState, undefined);
2020-05-24 04:38:24 +05:30
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount)]);
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount2)]);
});
it('should logout stranger accounts', () => {
logoutStrangers()(dispatch, getState, undefined);
expect(authentication.logout, 'to have calls satisfying', [
[foreignAccount.token],
[foreignAccount2.token],
]);
});
2020-05-24 04:38:24 +05:30
it('should activate another account if available', () =>
logoutStrangers()(dispatch, getState, undefined).then(() =>
expect(dispatch, 'to have a call satisfying', [activate(account)]),
));
it('should not activate another account if active account is already not a stranger', () => {
(getState as any).returns({
accounts: {
active: account.id,
available: [account, foreignAccount],
},
user,
});
return logoutStrangers()(dispatch, getState, undefined).then(() =>
expect(dispatch, 'not to have calls satisfying', [activate(account)]),
);
});
2020-05-24 04:38:24 +05:30
it('should not dispatch if no strangers', () => {
(getState as any).returns({
accounts: {
active: account.id,
available: [account],
},
user,
});
2020-05-24 04:38:24 +05:30
return logoutStrangers()(dispatch, getState, undefined).then(() => expect(dispatch, 'was not called'));
});
2020-05-24 04:38:24 +05:30
describe('when all accounts are strangers', () => {
beforeEach(() => {
(getState as any).returns({
accounts: {
active: foreignAccount.id,
available: [foreignAccount, foreignAccount2],
},
auth: {
credentials: {},
},
user,
});
logoutStrangers()(dispatch, getState, undefined);
});
it('logouts all accounts', () => {
expect(authentication.logout, 'to have calls satisfying', [
[foreignAccount.token],
[foreignAccount2.token],
]);
expect(dispatch, 'to have a call satisfying', [setUser({ isGuest: true })]);
expect(dispatch, 'to have a call satisfying', [reset()]);
});
});
2020-05-24 04:38:24 +05:30
describe('when a stranger has a mark in sessionStorage', () => {
const key = `stranger${foreignAccount.id}`;
2020-05-24 04:38:24 +05:30
beforeEach(() => {
sessionStorage.setItem(key, '1');
2020-05-24 04:38:24 +05:30
logoutStrangers()(dispatch, getState, undefined);
});
2020-05-24 04:38:24 +05:30
afterEach(() => {
sessionStorage.removeItem(key);
});
2020-05-24 04:38:24 +05:30
it('should not log out', () =>
expect(dispatch, 'not to have calls satisfying', [{ payload: foreignAccount }]));
});
});
});