mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-05 04:59:26 +05:30
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
import expect from 'app/test/unexpected';
|
|
import sinon, { SinonStub } from 'sinon';
|
|
|
|
import AuthFlow from 'app/services/authFlow/AuthFlow';
|
|
import AbstractState from 'app/services/authFlow/AbstractState';
|
|
import localStorage from 'app/services/localStorage';
|
|
|
|
import OAuthState from 'app/services/authFlow/OAuthState';
|
|
import RegisterState from 'app/services/authFlow/RegisterState';
|
|
import RecoverPasswordState from 'app/services/authFlow/RecoverPasswordState';
|
|
import ForgotPasswordState from 'app/services/authFlow/ForgotPasswordState';
|
|
import ActivationState from 'app/services/authFlow/ActivationState';
|
|
import ResendActivationState from 'app/services/authFlow/ResendActivationState';
|
|
import LoginState from 'app/services/authFlow/LoginState';
|
|
import CompleteState from 'app/services/authFlow/CompleteState';
|
|
import ChooseAccountState from 'app/services/authFlow/ChooseAccountState';
|
|
import { Store } from 'redux';
|
|
|
|
describe('AuthFlow', () => {
|
|
let flow: AuthFlow;
|
|
let actions: { test: SinonStub };
|
|
|
|
beforeEach(() => {
|
|
actions = {
|
|
test: sinon.stub().named('actions.test'),
|
|
};
|
|
actions.test.returns('passed');
|
|
|
|
// @ts-ignore
|
|
flow = new AuthFlow(actions);
|
|
});
|
|
|
|
it('throws when no actions provided', () => {
|
|
expect(
|
|
// @ts-ignore
|
|
() => new AuthFlow(),
|
|
'to throw',
|
|
'AuthFlow requires an actions object',
|
|
);
|
|
});
|
|
|
|
it('should not allow to mutate actions', () => {
|
|
expect(
|
|
// @ts-ignore
|
|
() => (flow.actions.foo = 'bar'),
|
|
'to throw',
|
|
/readonly|not extensible/,
|
|
);
|
|
// @ts-ignore
|
|
expect(() => (flow.actions.test = 'hacked'), 'to throw', /read ?only/);
|
|
});
|
|
|
|
describe('#setStore', () => {
|
|
it('should create #navigate, #getState, #dispatch', () => {
|
|
flow.setStore({
|
|
// @ts-ignore
|
|
getState() {},
|
|
// @ts-ignore
|
|
dispatch() {},
|
|
});
|
|
|
|
expect(flow.getState, 'to be defined');
|
|
expect(flow.dispatch, 'to be defined');
|
|
expect(flow.navigate, 'to be defined');
|
|
});
|
|
});
|
|
|
|
describe('#restoreOAuthState', () => {
|
|
let oauthData: Record<string, string>;
|
|
|
|
beforeEach(() => {
|
|
oauthData = { foo: 'bar' };
|
|
localStorage.setItem(
|
|
'oauthData',
|
|
JSON.stringify({
|
|
timestamp: Date.now() - 10,
|
|
payload: oauthData,
|
|
}),
|
|
);
|
|
|
|
sinon.stub(flow, 'run').named('flow.run');
|
|
const promiseLike = { then: (fn: Function) => fn() || promiseLike };
|
|
// @ts-ignore
|
|
flow.run.returns(promiseLike);
|
|
sinon.stub(flow, 'setState').named('flow.setState');
|
|
});
|
|
|
|
afterEach(() => {
|
|
localStorage.removeItem('oauthData');
|
|
});
|
|
|
|
it('should call to restoreOAuthState', () => {
|
|
// @ts-ignore
|
|
sinon.stub(flow, 'restoreOAuthState').named('flow.restoreOAuthState');
|
|
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
// @ts-ignore
|
|
expect(flow.restoreOAuthState, 'was called');
|
|
});
|
|
|
|
it('should restore oauth state from localStorage', () => {
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.run, 'to have a call satisfying', [
|
|
'oAuthValidate',
|
|
oauthData,
|
|
]);
|
|
});
|
|
|
|
it('should transition to CompleteState', () => {
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.setState, 'to have a call satisfying', [
|
|
expect.it('to be a', CompleteState),
|
|
]);
|
|
});
|
|
|
|
it('should not handle current request', () => {
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.setState, 'was called once');
|
|
});
|
|
|
|
it('should call onReady after state restoration', () => {
|
|
const onReady = sinon.stub().named('onReady');
|
|
|
|
flow.handleRequest(
|
|
{ path: '/login', query: new URLSearchParams(''), params: {} },
|
|
// @ts-ignore
|
|
null,
|
|
onReady,
|
|
);
|
|
|
|
expect(onReady, 'was called');
|
|
});
|
|
|
|
it('should not restore oauth state for /register route', () => {
|
|
flow.handleRequest(
|
|
{ path: '/register', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.run, 'was not called'); // this.run('oAuthValidate'...
|
|
});
|
|
|
|
it('should not restore outdated (>1h) oauth state', () => {
|
|
localStorage.setItem(
|
|
'oauthData',
|
|
JSON.stringify({
|
|
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
|
payload: oauthData,
|
|
}),
|
|
);
|
|
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams(''), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.run, 'was not called');
|
|
});
|
|
});
|
|
|
|
describe('#setState', () => {
|
|
it('should change state', () => {
|
|
const state = new AbstractState();
|
|
flow.setState(state);
|
|
|
|
expect(flow.state, 'to be', state);
|
|
});
|
|
|
|
it('should call #enter() on new state and pass reference to itself', () => {
|
|
const state = new AbstractState();
|
|
const spy = sinon.spy(state, 'enter').named('state.enter');
|
|
|
|
flow.setState(state);
|
|
|
|
expect(spy, 'was called once');
|
|
expect(spy, 'to have a call satisfying', [flow]);
|
|
});
|
|
|
|
it('should call `leave` on previous state if any', () => {
|
|
class State1 extends AbstractState {}
|
|
class State2 extends AbstractState {}
|
|
|
|
const state1 = new State1();
|
|
const state2 = new State2();
|
|
const spy1 = sinon.spy(state1, 'leave');
|
|
const spy2 = sinon.spy(state2, 'leave');
|
|
|
|
flow.setState(state1);
|
|
flow.setState(state2);
|
|
|
|
expect(spy1, 'was called once');
|
|
expect(spy1, 'to have a call satisfying', [flow]);
|
|
expect(spy2, 'was not called');
|
|
});
|
|
|
|
it('should return promise, if #enter returns it', () => {
|
|
const state = new AbstractState();
|
|
const expected = Promise.resolve();
|
|
|
|
state.enter = () => expected;
|
|
|
|
const actual = flow.setState(state);
|
|
|
|
expect(actual, 'to be', expected);
|
|
});
|
|
|
|
it('should throw if no state', () => {
|
|
// @ts-ignore
|
|
expect(() => flow.setState(), 'to throw', 'State is required');
|
|
});
|
|
});
|
|
|
|
describe('#run', () => {
|
|
let store: Store;
|
|
|
|
beforeEach(() => {
|
|
// @ts-ignore
|
|
store = {
|
|
getState() {},
|
|
dispatch: sinon.stub().named('store.dispatch'),
|
|
};
|
|
|
|
flow.setStore(store);
|
|
});
|
|
|
|
it('should dispatch an action', () => {
|
|
// @ts-ignore
|
|
flow.run('test');
|
|
|
|
expect(store.dispatch, 'was called once');
|
|
expect(store.dispatch, 'to have a call satisfying', ['passed']);
|
|
});
|
|
|
|
it('should dispatch an action with payload given', () => {
|
|
// @ts-ignore
|
|
flow.run('test', 'arg');
|
|
|
|
expect(actions.test, 'was called once');
|
|
expect(actions.test, 'to have a call satisfying', ['arg']);
|
|
});
|
|
|
|
it('should resolve to action dispatch result', () => {
|
|
const expected = 'dispatch called';
|
|
// @ts-ignore
|
|
store.dispatch.returns(expected);
|
|
|
|
// @ts-ignore
|
|
return expect(flow.run('test'), 'to be fulfilled with', expected);
|
|
});
|
|
|
|
it('throws when running unexisted action', () => {
|
|
// @ts-ignore
|
|
expect(() => flow.run('123'), 'to throw', 'Action 123 does not exists');
|
|
});
|
|
});
|
|
|
|
describe('#goBack', () => {
|
|
it('should call goBack on state passing itself as argument', () => {
|
|
const state = new AbstractState();
|
|
sinon.stub(state, 'goBack').named('state.goBack');
|
|
flow.setState(state);
|
|
|
|
flow.goBack();
|
|
|
|
expect(state.goBack, 'was called once');
|
|
expect(state.goBack, 'to have a call satisfying', [flow]);
|
|
});
|
|
});
|
|
|
|
describe('#resolve', () => {
|
|
it('should call resolve on state passing itself and payload as arguments', () => {
|
|
const state = new AbstractState();
|
|
sinon.stub(state, 'resolve').named('state.resolve');
|
|
flow.setState(state);
|
|
|
|
const expectedPayload = { foo: 'bar' };
|
|
|
|
flow.resolve(expectedPayload);
|
|
|
|
expect(state.resolve, 'was called once');
|
|
expect(state.resolve, 'to have a call satisfying', [
|
|
flow,
|
|
expectedPayload,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('#reject', () => {
|
|
it('should call reject on state passing itself and payload as arguments', () => {
|
|
const state = new AbstractState();
|
|
sinon.stub(state, 'reject').named('state.reject');
|
|
flow.setState(state);
|
|
|
|
const expectedPayload = { foo: 'bar' };
|
|
|
|
flow.reject(expectedPayload);
|
|
|
|
expect(state.reject, 'was called once');
|
|
expect(state.reject, 'to have a call satisfying', [
|
|
flow,
|
|
expectedPayload,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('#handleRequest()', () => {
|
|
beforeEach(() => {
|
|
sinon.stub(flow, 'setState').named('flow.setState');
|
|
sinon.stub(flow, 'run').named('flow.run');
|
|
});
|
|
|
|
Object.entries({
|
|
'/': LoginState,
|
|
'/login': LoginState,
|
|
'/password': LoginState,
|
|
'/accept-rules': LoginState,
|
|
'/oauth/permissions': LoginState,
|
|
'/oauth/choose-account': LoginState,
|
|
'/oauth/finish': LoginState,
|
|
'/oauth2/v1/foo': OAuthState,
|
|
'/oauth2/v1': OAuthState,
|
|
'/oauth2': OAuthState,
|
|
'/register': RegisterState,
|
|
'/choose-account': ChooseAccountState,
|
|
'/recover-password': RecoverPasswordState,
|
|
'/recover-password/key123': RecoverPasswordState,
|
|
'/forgot-password': ForgotPasswordState,
|
|
'/activation': ActivationState,
|
|
'/resend-activation': ResendActivationState,
|
|
}).forEach(([path, type]) => {
|
|
it(`should transition to ${type.name} if ${path}`, () => {
|
|
flow.handleRequest(
|
|
{ path, query: new URLSearchParams({}), params: {} },
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.setState, 'was called once');
|
|
expect(flow.setState, 'to have a call satisfying', [
|
|
expect.it('to be a', type),
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('should call callback', () => {
|
|
const callback = sinon.stub().named('callback');
|
|
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams({}), params: {} },
|
|
() => {},
|
|
callback,
|
|
);
|
|
|
|
expect(callback, 'was called once');
|
|
});
|
|
|
|
it('should not call callback till returned from #enter() promise will be resolved', () => {
|
|
let resolve: Function;
|
|
const promise: Promise<void> = {
|
|
// @ts-ignore
|
|
then: (cb: Function) => {
|
|
resolve = cb;
|
|
},
|
|
};
|
|
const callback = sinon.stub().named('callback');
|
|
|
|
const state = new AbstractState();
|
|
state.enter = () => promise;
|
|
|
|
flow.setState = AuthFlow.prototype.setState.bind(flow, state);
|
|
|
|
flow.handleRequest(
|
|
{ path: '/', query: new URLSearchParams({}), params: {} },
|
|
() => {},
|
|
callback,
|
|
);
|
|
|
|
// @ts-ignore
|
|
expect(resolve, 'to be', callback);
|
|
|
|
expect(callback, 'was not called');
|
|
// @ts-ignore
|
|
resolve();
|
|
expect(callback, 'was called once');
|
|
});
|
|
|
|
it('should not handle the same request twice', () => {
|
|
const path = '/oauth2';
|
|
const callback = sinon.stub();
|
|
|
|
flow.handleRequest(
|
|
{ path, query: new URLSearchParams({}), params: {} },
|
|
() => {},
|
|
callback,
|
|
);
|
|
flow.handleRequest(
|
|
{ path, query: new URLSearchParams({}), params: {} },
|
|
() => {},
|
|
callback,
|
|
);
|
|
|
|
expect(flow.setState, 'was called once');
|
|
expect(flow.setState, 'to have a call satisfying', [
|
|
expect.it('to be a', OAuthState),
|
|
]);
|
|
expect(callback, 'was called twice');
|
|
});
|
|
|
|
it('throws if unsupported request', () => {
|
|
const replace = sinon.stub().named('replace');
|
|
|
|
flow.handleRequest(
|
|
{ path: '/foo/bar', query: new URLSearchParams({}), params: {} },
|
|
replace,
|
|
);
|
|
|
|
expect(replace, 'to have a call satisfying', ['/404']);
|
|
});
|
|
});
|
|
|
|
describe('#getRequest()', () => {
|
|
beforeEach(() => {
|
|
sinon.stub(flow, 'setState').named('flow.setState');
|
|
sinon.stub(flow, 'run').named('flow.run');
|
|
});
|
|
|
|
it('should return request with path, query, params', () => {
|
|
const request = { path: '/', query: new URLSearchParams({}), params: {} };
|
|
|
|
flow.handleRequest(
|
|
request,
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.getRequest(), 'to satisfy', {
|
|
...request,
|
|
query: expect.it('to be an', URLSearchParams),
|
|
params: {},
|
|
});
|
|
});
|
|
|
|
it('should return a copy of current request', () => {
|
|
const request = {
|
|
path: '/',
|
|
query: new URLSearchParams({ foo: 'bar' }),
|
|
params: { baz: 'bud' },
|
|
};
|
|
|
|
flow.handleRequest(
|
|
request,
|
|
() => {},
|
|
() => {},
|
|
);
|
|
|
|
expect(flow.getRequest(), 'to equal', request);
|
|
expect(flow.getRequest(), 'not to be', request);
|
|
});
|
|
});
|
|
});
|