mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Implemented strict mode for the project (broken tests, hundreds of @ts-ignore and new errors are included) [skip ci]
This commit is contained in:
committed by
SleepWalker
parent
10e8b77acf
commit
96049ad4ad
@@ -2,7 +2,7 @@ import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import request from 'app/services/request';
|
||||
import signup from 'app/services/api/signup';
|
||||
import * as signup from 'app/services/api/signup';
|
||||
|
||||
describe('signup api', () => {
|
||||
describe('#register', () => {
|
||||
@@ -46,9 +46,7 @@ describe('signup api', () => {
|
||||
});
|
||||
|
||||
describe('#activate', () => {
|
||||
const params = {
|
||||
key: 'key',
|
||||
};
|
||||
const key = 'key';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(request, 'post').named('request.post');
|
||||
@@ -59,21 +57,21 @@ describe('signup api', () => {
|
||||
});
|
||||
|
||||
it('should post to confirmation api', () => {
|
||||
signup.activate(params);
|
||||
signup.activate(key);
|
||||
|
||||
expect(request.post, 'to have a call satisfying', [
|
||||
'/api/signup/confirm',
|
||||
params,
|
||||
{ key },
|
||||
{},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should disable any token', () => {
|
||||
signup.activate(params);
|
||||
signup.activate(key);
|
||||
|
||||
expect(request.post, 'to have a call satisfying', [
|
||||
'/api/signup/confirm',
|
||||
params,
|
||||
{ key },
|
||||
{ token: null },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonFakeServer } from 'sinon';
|
||||
|
||||
import request from 'app/services/request';
|
||||
import * as authentication from 'app/services/api/authentication';
|
||||
import * as accounts from 'app/services/api/accounts';
|
||||
|
||||
describe('authentication api', () => {
|
||||
let server;
|
||||
let server: SinonFakeServer;
|
||||
|
||||
beforeEach(() => {
|
||||
server = sinon.createFakeServer({
|
||||
server = sinon.fakeServer.create({
|
||||
autoRespond: true,
|
||||
});
|
||||
|
||||
['get', 'post'].forEach(method => {
|
||||
server[method] = (url, resp = {}, status = 200, headers = {}) => {
|
||||
server.respondWith(method, url, [
|
||||
status,
|
||||
{ 'Content-Type': 'application/json', ...headers },
|
||||
JSON.stringify(resp),
|
||||
]);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -156,12 +146,16 @@ describe('authentication api', () => {
|
||||
});
|
||||
|
||||
it('resolves with new token and user object', async () => {
|
||||
server.post('/api/authentication/refresh-token', {
|
||||
access_token: newToken,
|
||||
refresh_token: validRefreshToken,
|
||||
success: true,
|
||||
expires_in: 50000,
|
||||
});
|
||||
server.respondWith(
|
||||
'POST',
|
||||
'/api/authentication/refresh-token',
|
||||
JSON.stringify({
|
||||
access_token: newToken,
|
||||
refresh_token: validRefreshToken,
|
||||
success: true,
|
||||
expires_in: 50000,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
authentication.validateToken(...validateTokenArgs),
|
||||
@@ -178,7 +172,11 @@ describe('authentication api', () => {
|
||||
|
||||
it('rejects if token request failed', () => {
|
||||
const error = { error: 'Unexpected error example' };
|
||||
server.post('/api/authentication/refresh-token', error, 500);
|
||||
server.respondWith('POST', '/api/authentication/refresh-token', [
|
||||
500,
|
||||
[],
|
||||
JSON.stringify(error),
|
||||
]);
|
||||
|
||||
return expect(
|
||||
authentication.validateToken(...validateTokenArgs),
|
||||
@@ -205,12 +203,16 @@ describe('authentication api', () => {
|
||||
});
|
||||
|
||||
it('resolves with new token and user object', async () => {
|
||||
server.post('/api/authentication/refresh-token', {
|
||||
access_token: newToken,
|
||||
refresh_token: validRefreshToken,
|
||||
success: true,
|
||||
expires_in: 50000,
|
||||
});
|
||||
server.respondWith(
|
||||
'POST',
|
||||
'/api/authentication/refresh-token',
|
||||
JSON.stringify({
|
||||
access_token: newToken,
|
||||
refresh_token: validRefreshToken,
|
||||
success: true,
|
||||
expires_in: 50000,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
authentication.validateToken(...validateTokenArgs),
|
||||
@@ -227,7 +229,11 @@ describe('authentication api', () => {
|
||||
|
||||
it('rejects if token request failed', () => {
|
||||
const error = { error: 'Unexpected error example' };
|
||||
server.post('/api/authentication/refresh-token', error, 500);
|
||||
server.respondWith('POST', '/api/authentication/refresh-token', [
|
||||
500,
|
||||
[],
|
||||
JSON.stringify(error),
|
||||
]);
|
||||
|
||||
return expect(
|
||||
authentication.validateToken(...validateTokenArgs),
|
||||
|
||||
@@ -8,13 +8,13 @@ export type Scope =
|
||||
| 'account_info'
|
||||
| 'account_email';
|
||||
|
||||
export type Client = {
|
||||
export interface Client {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type OauthAppResponse = {
|
||||
export interface OauthAppResponse {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
type: ApplicationType;
|
||||
@@ -27,9 +27,9 @@ export type OauthAppResponse = {
|
||||
redirectUri?: string;
|
||||
// fields for 'minecraft-server' type
|
||||
minecraftServerIp?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type OauthRequestData = {
|
||||
interface OauthRequestData {
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
response_type: string;
|
||||
@@ -38,37 +38,43 @@ type OauthRequestData = {
|
||||
prompt: string;
|
||||
login_hint?: string;
|
||||
state?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type OauthData = {
|
||||
export interface OauthData {
|
||||
clientId: string;
|
||||
redirectUrl: string;
|
||||
responseType: string;
|
||||
description?: string;
|
||||
scope: string;
|
||||
// TODO: why prompt is not nullable?
|
||||
prompt: string; // comma separated list of 'none' | 'consent' | 'select_account';
|
||||
loginHint?: string;
|
||||
state?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type FormPayloads = {
|
||||
export interface OAuthValidateResponse {
|
||||
session: {
|
||||
scopes: Scope[];
|
||||
};
|
||||
client: Client;
|
||||
oAuth: {}; // TODO: improve typing
|
||||
}
|
||||
|
||||
interface FormPayloads {
|
||||
name?: string;
|
||||
description?: string;
|
||||
websiteUrl?: string;
|
||||
redirectUri?: string;
|
||||
minecraftServerIp?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const api = {
|
||||
validate(oauthData: OauthData) {
|
||||
return request
|
||||
.get<{
|
||||
session: {
|
||||
scopes: Scope[];
|
||||
};
|
||||
client: Client;
|
||||
oAuth: {};
|
||||
}>('/api/oauth2/v1/validate', getOAuthRequest(oauthData))
|
||||
.get<OAuthValidateResponse>(
|
||||
'/api/oauth2/v1/validate',
|
||||
getOAuthRequest(oauthData),
|
||||
)
|
||||
.catch(handleOauthParamsValidation);
|
||||
},
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ describe('services/api/options', () => {
|
||||
sinon
|
||||
.stub(request, 'get')
|
||||
.named('request.get')
|
||||
.returns(Promise.resolve(expectedResp));
|
||||
.returns(
|
||||
Promise.resolve({
|
||||
originalResponse: new Response(),
|
||||
...expectedResp,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
import request from 'app/services/request';
|
||||
import request, { Resp } from 'app/services/request';
|
||||
|
||||
import { OAuthResponse } from './authentication';
|
||||
|
||||
export default {
|
||||
register({
|
||||
email = '',
|
||||
username = '',
|
||||
password = '',
|
||||
rePassword = '',
|
||||
rulesAgreement = false,
|
||||
lang = '',
|
||||
captcha = '',
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/signup',
|
||||
{ email, username, password, rePassword, rulesAgreement, lang, captcha },
|
||||
{ token: null },
|
||||
);
|
||||
},
|
||||
interface RegisterParams {
|
||||
email?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
rePassword?: string;
|
||||
rulesAgreement?: boolean;
|
||||
lang?: string;
|
||||
captcha?: string;
|
||||
}
|
||||
|
||||
activate({ key = '' }) {
|
||||
return request.post<OAuthResponse>(
|
||||
'/api/signup/confirm',
|
||||
{ key },
|
||||
{ token: null },
|
||||
);
|
||||
},
|
||||
export function register({
|
||||
email = '',
|
||||
username = '',
|
||||
password = '',
|
||||
rePassword = '',
|
||||
rulesAgreement = false,
|
||||
lang = '',
|
||||
captcha = '',
|
||||
}: RegisterParams): Promise<Resp<void>> {
|
||||
return request.post(
|
||||
'/api/signup',
|
||||
{ email, username, password, rePassword, rulesAgreement, lang, captcha },
|
||||
{ token: null },
|
||||
);
|
||||
}
|
||||
|
||||
resendActivation({ email = '', captcha }) {
|
||||
return request.post('/api/signup/repeat-message', { email, captcha });
|
||||
},
|
||||
};
|
||||
export function activate(key: string = ''): Promise<Resp<OAuthResponse>> {
|
||||
return request.post('/api/signup/confirm', { key }, { token: null });
|
||||
}
|
||||
|
||||
export function resendActivation(email: string = '', captcha: string = '') {
|
||||
return request.post('/api/signup/repeat-message', { email, captcha });
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import { AuthContext } from 'app/services/authFlow';
|
||||
|
||||
export default class AbstractState {
|
||||
resolve(context: AuthContext, payload: { [key: string]: any }): any {}
|
||||
goBack(context: AuthContext): any {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {}
|
||||
goBack(context: AuthContext): void {
|
||||
throw new Error('There is no way back');
|
||||
}
|
||||
reject(context: AuthContext, payload: { [key: string]: any }): any {}
|
||||
enter(context: AuthContext): any {}
|
||||
leave(context: AuthContext): any {}
|
||||
reject(context: AuthContext, payload?: Record<string, any>): void {}
|
||||
enter(context: AuthContext): Promise<void> | void {}
|
||||
leave(context: AuthContext): void {}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import AcceptRulesState from 'app/services/authFlow/AcceptRulesState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import { SinonMock } from 'sinon';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('AcceptRulesState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: AcceptRulesState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new AcceptRulesState();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import CompleteState from './CompleteState';
|
||||
|
||||
export default class AcceptRulesState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const { user } = context.getState();
|
||||
|
||||
if (user.shouldAcceptRules) {
|
||||
@@ -14,7 +15,7 @@ export default class AcceptRulesState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context) {
|
||||
resolve(context: AuthContext): Promise<void> | void {
|
||||
context
|
||||
.run('acceptRules')
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
@@ -23,7 +24,7 @@ export default class AcceptRulesState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext): void {
|
||||
context.run('logout');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import ActivationState from 'app/services/authFlow/ActivationState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import ResendActivationState from 'app/services/authFlow/ResendActivationState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('ActivationState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: ActivationState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new ActivationState();
|
||||
@@ -72,7 +78,7 @@ describe('ActivationState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
expectState(mock, CompleteState);
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise;
|
||||
});
|
||||
@@ -83,7 +89,7 @@ describe('ActivationState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
mock.expects('setState').never();
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
@@ -6,14 +6,17 @@ import CompleteState from './CompleteState';
|
||||
import ResendActivationState from './ResendActivationState';
|
||||
|
||||
export default class ActivationState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const url = context.getRequest().path.includes('/activation')
|
||||
? context.getRequest().path
|
||||
: '/activation';
|
||||
context.navigate(url);
|
||||
}
|
||||
|
||||
resolve(context: AuthContext, payload: { [key: string]: any }) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('activate', payload)
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
@@ -23,7 +26,7 @@ export default class ActivationState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
reject(context: AuthContext) {
|
||||
reject(context: AuthContext): void {
|
||||
context.setState(new ResendActivationState());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import AuthFlow from 'app/services/authFlow/AuthFlow';
|
||||
|
||||
import RegisterState from 'app/services/authFlow/RegisterState';
|
||||
@@ -8,20 +10,22 @@ import ActivationState from 'app/services/authFlow/ActivationState';
|
||||
import ResendActivationState from 'app/services/authFlow/ResendActivationState';
|
||||
|
||||
describe('AuthFlow.functional', () => {
|
||||
let flow;
|
||||
let actions;
|
||||
let store;
|
||||
let state;
|
||||
let navigate;
|
||||
let flow: AuthFlow;
|
||||
let actions: {};
|
||||
let store: Store;
|
||||
let state: { user?: { isGuest: boolean; isActive: boolean } };
|
||||
let navigate: (path: string, extra?: {}) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {};
|
||||
store = {
|
||||
getState: sinon.stub().named('store.getState'),
|
||||
// @ts-ignore
|
||||
dispatch: sinon
|
||||
.spy(({ type, payload = {} }) => {
|
||||
if (type === '@@router/TRANSITION' && payload.method === 'push') {
|
||||
// emulate redux-router
|
||||
// @ts-ignore
|
||||
navigate(...payload.args);
|
||||
}
|
||||
})
|
||||
@@ -30,6 +34,7 @@ describe('AuthFlow.functional', () => {
|
||||
|
||||
state = {};
|
||||
|
||||
// @ts-ignore
|
||||
flow = new AuthFlow(actions);
|
||||
flow.setStore(store);
|
||||
|
||||
@@ -48,6 +53,7 @@ describe('AuthFlow.functional', () => {
|
||||
|
||||
sinon.stub(flow, 'run').named('flow.run');
|
||||
sinon.spy(flow, 'navigate').named('flow.navigate');
|
||||
// @ts-ignore
|
||||
store.getState.returns(state);
|
||||
});
|
||||
|
||||
@@ -107,9 +113,11 @@ describe('AuthFlow.functional', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
flow.run.onCall(0).returns({ then: fn => fn() });
|
||||
// @ts-ignore
|
||||
flow.run.onCall(1).returns({
|
||||
then: fn =>
|
||||
then: (fn: Function) =>
|
||||
fn({
|
||||
redirectUri: expectedRedirect,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonStub } from 'sinon';
|
||||
|
||||
import AuthFlow from 'app/services/authFlow/AuthFlow';
|
||||
import AbstractState from 'app/services/authFlow/AbstractState';
|
||||
@@ -14,10 +14,11 @@ 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;
|
||||
let actions;
|
||||
let flow: AuthFlow;
|
||||
let actions: { test: SinonStub };
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
@@ -25,6 +26,7 @@ describe('AuthFlow', () => {
|
||||
};
|
||||
actions.test.returns('passed');
|
||||
|
||||
// @ts-ignore
|
||||
flow = new AuthFlow(actions);
|
||||
});
|
||||
|
||||
@@ -39,17 +41,21 @@ describe('AuthFlow', () => {
|
||||
|
||||
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() {},
|
||||
});
|
||||
|
||||
@@ -60,7 +66,7 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
describe('#restoreOAuthState', () => {
|
||||
let oauthData;
|
||||
let oauthData: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
oauthData = { foo: 'bar' };
|
||||
@@ -73,7 +79,8 @@ describe('AuthFlow', () => {
|
||||
);
|
||||
|
||||
sinon.stub(flow, 'run').named('flow.run');
|
||||
const promiseLike = { then: fn => fn() || promiseLike };
|
||||
const promiseLike = { then: (fn: Function) => fn() || promiseLike };
|
||||
// @ts-ignore
|
||||
flow.run.returns(promiseLike);
|
||||
sinon.stub(flow, 'setState').named('flow.setState');
|
||||
});
|
||||
@@ -83,15 +90,25 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should call to restoreOAuthState', () => {
|
||||
// @ts-ignore
|
||||
sinon.stub(flow, 'restoreOAuthState').named('flow.restoreOAuthState');
|
||||
|
||||
flow.handleRequest({ path: '/' });
|
||||
flow.handleRequest(
|
||||
{ path: '/', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
expect(flow.restoreOAuthState, 'was called');
|
||||
});
|
||||
|
||||
it('should restore oauth state from localStorage', () => {
|
||||
flow.handleRequest({ path: '/' });
|
||||
flow.handleRequest(
|
||||
{ path: '/', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.run, 'to have a call satisfying', [
|
||||
'oAuthValidate',
|
||||
@@ -100,7 +117,11 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should transition to CompleteState', () => {
|
||||
flow.handleRequest({ path: '/' });
|
||||
flow.handleRequest(
|
||||
{ path: '/', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.setState, 'to have a call satisfying', [
|
||||
expect.it('to be a', CompleteState),
|
||||
@@ -108,7 +129,11 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should not handle current request', () => {
|
||||
flow.handleRequest({ path: '/' });
|
||||
flow.handleRequest(
|
||||
{ path: '/', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.setState, 'was called once');
|
||||
});
|
||||
@@ -116,13 +141,22 @@ describe('AuthFlow', () => {
|
||||
it('should call onReady after state restoration', () => {
|
||||
const onReady = sinon.stub().named('onReady');
|
||||
|
||||
flow.handleRequest({ path: '/login' }, null, 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' });
|
||||
flow.handleRequest(
|
||||
{ path: '/register', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.run, 'was not called'); // this.run('oAuthValidate'...
|
||||
});
|
||||
@@ -136,7 +170,11 @@ describe('AuthFlow', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
flow.handleRequest({ path: '/' });
|
||||
flow.handleRequest(
|
||||
{ path: '/', query: new URLSearchParams(''), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.run, 'was not called');
|
||||
});
|
||||
@@ -189,14 +227,16 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should throw if no state', () => {
|
||||
// @ts-ignore
|
||||
expect(() => flow.setState(), 'to throw', 'State is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#run', () => {
|
||||
let store;
|
||||
let store: Store;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
store = {
|
||||
getState() {},
|
||||
dispatch: sinon.stub().named('store.dispatch'),
|
||||
@@ -206,6 +246,7 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should dispatch an action', () => {
|
||||
// @ts-ignore
|
||||
flow.run('test');
|
||||
|
||||
expect(store.dispatch, 'was called once');
|
||||
@@ -213,6 +254,7 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should dispatch an action with payload given', () => {
|
||||
// @ts-ignore
|
||||
flow.run('test', 'arg');
|
||||
|
||||
expect(actions.test, 'was called once');
|
||||
@@ -221,12 +263,15 @@ describe('AuthFlow', () => {
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -306,7 +351,11 @@ describe('AuthFlow', () => {
|
||||
'/resend-activation': ResendActivationState,
|
||||
}).forEach(([path, type]) => {
|
||||
it(`should transition to ${type.name} if ${path}`, () => {
|
||||
flow.handleRequest({ path });
|
||||
flow.handleRequest(
|
||||
{ path, query: new URLSearchParams({}), params: {} },
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.setState, 'was called once');
|
||||
expect(flow.setState, 'to have a call satisfying', [
|
||||
@@ -318,15 +367,20 @@ describe('AuthFlow', () => {
|
||||
it('should call callback', () => {
|
||||
const callback = sinon.stub().named('callback');
|
||||
|
||||
flow.handleRequest({ path: '/' }, () => {}, 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;
|
||||
const promise = {
|
||||
then: cb => {
|
||||
let resolve: Function;
|
||||
const promise: Promise<void> = {
|
||||
// @ts-ignore
|
||||
then: (cb: Function) => {
|
||||
resolve = cb;
|
||||
},
|
||||
};
|
||||
@@ -337,11 +391,17 @@ describe('AuthFlow', () => {
|
||||
|
||||
flow.setState = AuthFlow.prototype.setState.bind(flow, state);
|
||||
|
||||
flow.handleRequest({ path: '/' }, () => {}, callback);
|
||||
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');
|
||||
});
|
||||
@@ -350,8 +410,16 @@ describe('AuthFlow', () => {
|
||||
const path = '/oauth2';
|
||||
const callback = sinon.stub();
|
||||
|
||||
flow.handleRequest({ path }, () => {}, callback);
|
||||
flow.handleRequest({ path }, () => {}, callback);
|
||||
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', [
|
||||
@@ -363,7 +431,10 @@ describe('AuthFlow', () => {
|
||||
it('throws if unsupported request', () => {
|
||||
const replace = sinon.stub().named('replace');
|
||||
|
||||
flow.handleRequest({ path: '/foo/bar' }, replace);
|
||||
flow.handleRequest(
|
||||
{ path: '/foo/bar', query: new URLSearchParams({}), params: {} },
|
||||
replace,
|
||||
);
|
||||
|
||||
expect(replace, 'to have a call satisfying', ['/404']);
|
||||
});
|
||||
@@ -376,9 +447,13 @@ describe('AuthFlow', () => {
|
||||
});
|
||||
|
||||
it('should return request with path, query, params', () => {
|
||||
const request = { path: '/' };
|
||||
const request = { path: '/', query: new URLSearchParams({}), params: {} };
|
||||
|
||||
flow.handleRequest(request);
|
||||
flow.handleRequest(
|
||||
request,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.getRequest(), 'to satisfy', {
|
||||
...request,
|
||||
@@ -390,11 +465,15 @@ describe('AuthFlow', () => {
|
||||
it('should return a copy of current request', () => {
|
||||
const request = {
|
||||
path: '/',
|
||||
query: { foo: 'bar' },
|
||||
query: new URLSearchParams({ foo: 'bar' }),
|
||||
params: { baz: 'bud' },
|
||||
};
|
||||
|
||||
flow.handleRequest(request);
|
||||
flow.handleRequest(
|
||||
request,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(flow.getRequest(), 'to equal', request);
|
||||
expect(flow.getRequest(), 'not to be', request);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { browserHistory } from 'app/services/history';
|
||||
import logger from 'app/services/logger';
|
||||
import localStorage from 'app/services/localStorage';
|
||||
import { RootState } from 'app/reducers';
|
||||
import { RootState, Store } from 'app/reducers';
|
||||
|
||||
import RegisterState from './RegisterState';
|
||||
import LoginState from './LoginState';
|
||||
@@ -17,7 +17,7 @@ import AbstractState from './AbstractState';
|
||||
type Request = {
|
||||
path: string;
|
||||
query: URLSearchParams;
|
||||
params: { [key: string]: any };
|
||||
params: Record<string, any>;
|
||||
};
|
||||
|
||||
// TODO: temporary added to improve typing without major refactoring
|
||||
@@ -61,12 +61,13 @@ export interface AuthContext {
|
||||
prevState: AbstractState;
|
||||
}
|
||||
|
||||
export type ActionsDict = {
|
||||
[key: string]: (action: any) => { [key: string]: any };
|
||||
};
|
||||
export type ActionsDict = Record<
|
||||
ActionId,
|
||||
(action: any) => Record<string, any>
|
||||
>;
|
||||
|
||||
export default class AuthFlow implements AuthContext {
|
||||
actions: ActionsDict;
|
||||
actions: Readonly<ActionsDict>;
|
||||
state: AbstractState;
|
||||
prevState: AbstractState;
|
||||
/**
|
||||
@@ -78,33 +79,18 @@ export default class AuthFlow implements AuthContext {
|
||||
navigate: (route: string, options: { replace?: boolean }) => void;
|
||||
currentRequest: Request;
|
||||
oAuthStateRestored = false;
|
||||
dispatch: (action: { [key: string]: any }) => void;
|
||||
dispatch: (action: Record<string, any>) => void;
|
||||
getState: () => RootState;
|
||||
|
||||
constructor(actions: ActionsDict) {
|
||||
if (typeof actions !== 'object') {
|
||||
throw new Error('AuthFlow requires an actions object');
|
||||
}
|
||||
|
||||
this.actions = actions;
|
||||
|
||||
if (Object.freeze) {
|
||||
Object.freeze(this.actions);
|
||||
}
|
||||
this.actions = Object.freeze(actions);
|
||||
}
|
||||
|
||||
setStore(store: {
|
||||
getState: () => { [key: string]: any };
|
||||
dispatch: (
|
||||
action: { [key: string]: any } | ((...args: any[]) => any),
|
||||
) => void;
|
||||
}) {
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {object} options
|
||||
* @param {object} options.replace
|
||||
*/
|
||||
this.navigate = (route: string, options: { replace?: boolean } = {}) => {
|
||||
setStore(store: Store): void {
|
||||
this.navigate = (
|
||||
route: string,
|
||||
options: { replace?: boolean } = {},
|
||||
): void => {
|
||||
const { path: currentPath } = this.getRequest();
|
||||
|
||||
if (currentPath !== route) {
|
||||
@@ -141,7 +127,7 @@ export default class AuthFlow implements AuthContext {
|
||||
this.state.goBack(this);
|
||||
}
|
||||
|
||||
run(actionId: ActionId, payload?: { [key: string]: any }): Promise<any> {
|
||||
run(actionId: ActionId, payload?: Record<string, any>): Promise<any> {
|
||||
const action = this.actions[actionId];
|
||||
|
||||
if (!action) {
|
||||
@@ -295,7 +281,8 @@ export default class AuthFlow implements AuthContext {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem('oauthData'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const data = JSON.parse(localStorage.getItem('oauthData')!);
|
||||
const expirationTime = 2 * 60 * 60 * 1000; // 2h
|
||||
|
||||
if (Date.now() - data.timestamp < expirationTime) {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import ChooseAccountState from 'app/services/authFlow/ChooseAccountState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import LoginState from 'app/services/authFlow/LoginState';
|
||||
import { SinonMock } from 'sinon';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('ChooseAccountState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: ChooseAccountState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new ChooseAccountState();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import LoginState from './LoginState';
|
||||
import CompleteState from './CompleteState';
|
||||
|
||||
export default class ChooseAccountState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const { auth } = context.getState();
|
||||
|
||||
if (auth.oauth) {
|
||||
@@ -13,7 +14,10 @@ export default class ChooseAccountState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context, payload) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {
|
||||
if (payload.id) {
|
||||
context.setState(new CompleteState());
|
||||
} else {
|
||||
@@ -23,7 +27,7 @@ export default class ChooseAccountState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext): void {
|
||||
context.run('logout');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import LoginState from 'app/services/authFlow/LoginState';
|
||||
@@ -8,13 +8,21 @@ import AcceptRulesState from 'app/services/authFlow/AcceptRulesState';
|
||||
import FinishState from 'app/services/authFlow/FinishState';
|
||||
import PermissionsState from 'app/services/authFlow/PermissionsState';
|
||||
import ChooseAccountState from 'app/services/authFlow/ChooseAccountState';
|
||||
import { Account } from 'app/components/accounts/reducer';
|
||||
import AbstractState from './AbstractState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('CompleteState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: CompleteState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new CompleteState();
|
||||
@@ -289,7 +297,7 @@ describe('CompleteState', () => {
|
||||
});
|
||||
|
||||
expectRun(mock, 'oAuthComplete', sinon.match.object).returns({
|
||||
then(success, fail) {
|
||||
then(success: Function, fail: Function) {
|
||||
expect(success, 'to be a', 'function');
|
||||
expect(fail, 'to be a', 'function');
|
||||
},
|
||||
@@ -323,7 +331,12 @@ describe('CompleteState', () => {
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
const testOAuth = (type, resp, expectedInstance) => {
|
||||
const testOAuth = (
|
||||
type: 'resolve' | 'reject',
|
||||
resp: Record<string, any>,
|
||||
expectedInstance: typeof AbstractState,
|
||||
) => {
|
||||
// @ts-ignore
|
||||
const promise = Promise[type](resp);
|
||||
|
||||
context.getState.returns({
|
||||
@@ -364,11 +377,13 @@ describe('CompleteState', () => {
|
||||
testOAuth('reject', { acceptRequired: true }, PermissionsState));
|
||||
|
||||
describe('when loginHint is set', () => {
|
||||
const testSuccessLoginHint = field => {
|
||||
const account = {
|
||||
const testSuccessLoginHint = (field: keyof Account) => {
|
||||
const account: Account = {
|
||||
id: 9,
|
||||
email: 'some@email.com',
|
||||
username: 'thatUsername',
|
||||
token: '',
|
||||
refreshToken: '',
|
||||
};
|
||||
|
||||
context.getState.returns({
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class CompleteState extends AbstractState {
|
||||
this.isPermissionsAccepted = options.accept;
|
||||
}
|
||||
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const {
|
||||
auth: { oauth },
|
||||
user,
|
||||
@@ -43,7 +43,7 @@ export default class CompleteState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
processOAuth(context: AuthContext) {
|
||||
processOAuth(context: AuthContext): Promise<void> | void {
|
||||
const { auth, accounts } = context.getState();
|
||||
|
||||
let { isSwitcherEnabled } = auth;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import FinishState from 'app/services/authFlow/FinishState';
|
||||
import { SinonMock } from 'sinon';
|
||||
|
||||
import { bootstrap, expectNavigate } from './helpers';
|
||||
import { bootstrap, expectNavigate, MockedAuthContext } from './helpers';
|
||||
|
||||
describe('FinishState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: FinishState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new FinishState();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class FinishState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
context.navigate('/oauth/finish');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import ForgotPasswordState from 'app/services/authFlow/ForgotPasswordState';
|
||||
import RecoverPasswordState from 'app/services/authFlow/RecoverPasswordState';
|
||||
import LoginState from 'app/services/authFlow/LoginState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('ForgotPasswordState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: ForgotPasswordState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new ForgotPasswordState();
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import LoginState from './LoginState';
|
||||
import RecoverPasswordState from './RecoverPasswordState';
|
||||
|
||||
export default class ForgotPasswordState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
context.navigate('/forgot-password');
|
||||
}
|
||||
|
||||
resolve(
|
||||
context,
|
||||
context: AuthContext,
|
||||
payload: {
|
||||
login?: string;
|
||||
} = {},
|
||||
) {
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('forgotPassword', payload)
|
||||
.then(() => {
|
||||
@@ -28,11 +29,11 @@ export default class ForgotPasswordState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
goBack(context) {
|
||||
goBack(context: AuthContext): void {
|
||||
context.setState(new LoginState());
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext): void {
|
||||
context.setState(new RecoverPasswordState());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import LoginState from 'app/services/authFlow/LoginState';
|
||||
import PasswordState from 'app/services/authFlow/PasswordState';
|
||||
import RegisterState from 'app/services/authFlow/RegisterState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import { bootstrap, expectState, expectNavigate, expectRun, MockedAuthContext } from './helpers';
|
||||
|
||||
describe('LoginState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: LoginState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new LoginState();
|
||||
@@ -53,7 +53,7 @@ describe('LoginState', () => {
|
||||
|
||||
describe('#resolve', () => {
|
||||
it('should call login with email or username', () => {
|
||||
const payload = {};
|
||||
const payload = { login: 'login' };
|
||||
|
||||
expectRun(mock, 'login', sinon.match.same(payload)).returns(
|
||||
new Promise(() => {}),
|
||||
@@ -68,7 +68,7 @@ describe('LoginState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
expectState(mock, PasswordState);
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, { login: 'login' });
|
||||
|
||||
return promise;
|
||||
});
|
||||
@@ -79,7 +79,7 @@ describe('LoginState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
mock.expects('setState').never();
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, { login: 'login' });
|
||||
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import RegisterState from './RegisterState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class LoginState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const login = getLogin(context.getState());
|
||||
const { user } = context.getState();
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class LoginState extends AbstractState {
|
||||
payload: {
|
||||
login: string;
|
||||
},
|
||||
) {
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('login', payload)
|
||||
.then(() => context.setState(new PasswordState()))
|
||||
@@ -39,11 +39,11 @@ export default class LoginState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
reject(context: AuthContext) {
|
||||
reject(context: AuthContext): void {
|
||||
context.setState(new RegisterState());
|
||||
}
|
||||
|
||||
goBack(context: AuthContext) {
|
||||
goBack(context: AuthContext): void {
|
||||
context.run('goBack', {
|
||||
fallbackUrl: '/',
|
||||
});
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import MfaState from './MfaState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import PasswordState from 'app/services/authFlow/PasswordState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('MfaState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: MfaState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new MfaState();
|
||||
|
||||
@@ -8,7 +8,7 @@ import PasswordState from './PasswordState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class MfaState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const { login, password, isTotpRequired } = getCredentials(
|
||||
context.getState(),
|
||||
);
|
||||
@@ -20,7 +20,10 @@ export default class MfaState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context: AuthContext, { totp }: { totp: string }) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
{ totp }: { totp: string },
|
||||
): Promise<void> | void {
|
||||
const { login, password, rememberMe } = getCredentials(context.getState());
|
||||
|
||||
return context
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
/* eslint @typescript-eslint/camelcase: off */
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import OAuthState from 'app/services/authFlow/OAuthState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
|
||||
import { bootstrap, expectState, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('OAuthState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: OAuthState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new OAuthState();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import CompleteState from './CompleteState';
|
||||
|
||||
export default class OAuthState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const { query, params } = context.getRequest();
|
||||
|
||||
return context
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import PasswordState from 'app/services/authFlow/PasswordState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
@@ -8,12 +8,18 @@ import LoginState from 'app/services/authFlow/LoginState';
|
||||
import ForgotPasswordState from 'app/services/authFlow/ForgotPasswordState';
|
||||
import ChooseAccountState from 'app/services/authFlow/ChooseAccountState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('PasswordState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: PasswordState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new PasswordState();
|
||||
|
||||
@@ -14,7 +14,7 @@ import MfaState from './MfaState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
|
||||
export default class PasswordState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const { login, isTotpRequired } = getCredentials(context.getState());
|
||||
|
||||
if (isTotpRequired) {
|
||||
@@ -35,7 +35,7 @@ export default class PasswordState extends AbstractState {
|
||||
password: string;
|
||||
rememberMe: boolean;
|
||||
},
|
||||
) {
|
||||
): Promise<void> | void {
|
||||
const { login, returnUrl } = getCredentials(context.getState());
|
||||
|
||||
return context
|
||||
@@ -62,11 +62,11 @@ export default class PasswordState extends AbstractState {
|
||||
.catch((err = {}) => err.errors || logger.warn('Error logging in', err));
|
||||
}
|
||||
|
||||
reject(context: AuthContext) {
|
||||
reject(context: AuthContext): void {
|
||||
context.setState(new ForgotPasswordState());
|
||||
}
|
||||
|
||||
goBack(context: AuthContext) {
|
||||
goBack(context: AuthContext): void {
|
||||
const state = context.getState();
|
||||
const { isRelogin } = getCredentials(state);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import PermissionsState from 'app/services/authFlow/PermissionsState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
|
||||
import { bootstrap, expectNavigate } from './helpers';
|
||||
import { bootstrap, expectNavigate, MockedAuthContext } from './helpers';
|
||||
|
||||
describe('PermissionsState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: PermissionsState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new PermissionsState();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import CompleteState from './CompleteState';
|
||||
|
||||
export default class PermissionsState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
context.navigate('/oauth/permissions', {
|
||||
// replacing oauth entry point if currently on it
|
||||
// to allow user easy go-back action to client's site
|
||||
@@ -10,15 +11,15 @@ export default class PermissionsState extends AbstractState {
|
||||
});
|
||||
}
|
||||
|
||||
resolve(context) {
|
||||
resolve(context: AuthContext): Promise<void> | void {
|
||||
this.process(context, true);
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext): void {
|
||||
this.process(context, false);
|
||||
}
|
||||
|
||||
process(context, accept) {
|
||||
process(context: AuthContext, accept: boolean): void {
|
||||
context.setState(
|
||||
new CompleteState({
|
||||
accept,
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import RecoverPasswordState from 'app/services/authFlow/RecoverPasswordState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import LoginState from 'app/services/authFlow/LoginState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('RecoverPasswordState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: RecoverPasswordState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new RecoverPasswordState();
|
||||
@@ -77,7 +83,7 @@ describe('RecoverPasswordState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
mock.expects('setState').never();
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import LoginState from './LoginState';
|
||||
import CompleteState from './CompleteState';
|
||||
|
||||
export default class RecoverPasswordState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
const url = context.getRequest().path.includes('/recover-password')
|
||||
? context.getRequest().path
|
||||
: '/recover-password';
|
||||
context.navigate(url);
|
||||
}
|
||||
|
||||
resolve(context, payload) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('recoverPassword', payload)
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
@@ -22,11 +26,11 @@ export default class RecoverPasswordState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
goBack(context) {
|
||||
goBack(context: AuthContext): void {
|
||||
context.setState(new LoginState());
|
||||
}
|
||||
|
||||
reject(context) {
|
||||
reject(context: AuthContext): void {
|
||||
context.run('contactUs');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import RegisterState from 'app/services/authFlow/RegisterState';
|
||||
import CompleteState from 'app/services/authFlow/CompleteState';
|
||||
import ActivationState from 'app/services/authFlow/ActivationState';
|
||||
import ResendActivationState from 'app/services/authFlow/ResendActivationState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('RegisterState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: RegisterState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new RegisterState();
|
||||
@@ -65,7 +71,7 @@ describe('RegisterState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
mock.expects('setState').never();
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
import CompleteState from './CompleteState';
|
||||
import ActivationState from './ActivationState';
|
||||
import ResendActivationState from './ResendActivationState';
|
||||
|
||||
export default class RegisterState extends AbstractState {
|
||||
enter(context) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
context.navigate('/register');
|
||||
}
|
||||
|
||||
resolve(context, payload) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('register', payload)
|
||||
.then(() => context.setState(new CompleteState()))
|
||||
.catch((err = {}) => err.errors || logger.warn('Error registering', err));
|
||||
}
|
||||
|
||||
reject(context, payload) {
|
||||
reject(context: AuthContext, payload: Record<string, any>): void {
|
||||
if (payload.requestEmail) {
|
||||
context.setState(new ResendActivationState());
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonMock } from 'sinon';
|
||||
|
||||
import ResendActivationState from 'app/services/authFlow/ResendActivationState';
|
||||
import ActivationState from 'app/services/authFlow/ActivationState';
|
||||
import RegisterState from 'app/services/authFlow/RegisterState';
|
||||
|
||||
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||
import {
|
||||
bootstrap,
|
||||
expectState,
|
||||
expectNavigate,
|
||||
expectRun,
|
||||
MockedAuthContext,
|
||||
} from './helpers';
|
||||
|
||||
describe('ResendActivationState', () => {
|
||||
let state;
|
||||
let context;
|
||||
let mock;
|
||||
let state: ResendActivationState;
|
||||
let context: MockedAuthContext;
|
||||
let mock: SinonMock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new ResendActivationState();
|
||||
@@ -67,7 +73,7 @@ describe('ResendActivationState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
expectState(mock, ActivationState);
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise;
|
||||
});
|
||||
@@ -78,7 +84,7 @@ describe('ResendActivationState', () => {
|
||||
mock.expects('run').returns(promise);
|
||||
mock.expects('setState').never();
|
||||
|
||||
state.resolve(context);
|
||||
state.resolve(context, {});
|
||||
|
||||
return promise.catch(mock.verify.bind(mock));
|
||||
});
|
||||
|
||||
@@ -6,11 +6,14 @@ import ActivationState from './ActivationState';
|
||||
import RegisterState from './RegisterState';
|
||||
|
||||
export default class ResendActivationState extends AbstractState {
|
||||
enter(context: AuthContext) {
|
||||
enter(context: AuthContext): Promise<void> | void {
|
||||
context.navigate('/resend-activation');
|
||||
}
|
||||
|
||||
resolve(context: AuthContext, payload: { [key: string]: any }) {
|
||||
resolve(
|
||||
context: AuthContext,
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> | void {
|
||||
context
|
||||
.run('resendActivation', payload)
|
||||
.then(() => context.setState(new ActivationState()))
|
||||
@@ -20,11 +23,11 @@ export default class ResendActivationState extends AbstractState {
|
||||
);
|
||||
}
|
||||
|
||||
reject(context: AuthContext) {
|
||||
reject(context: AuthContext): void {
|
||||
context.setState(new ActivationState());
|
||||
}
|
||||
|
||||
goBack(context: AuthContext) {
|
||||
goBack(context: AuthContext): void {
|
||||
if (context.prevState instanceof RegisterState) {
|
||||
context.setState(new RegisterState());
|
||||
} else {
|
||||
|
||||
@@ -2,15 +2,25 @@
|
||||
* A helpers for testing states in isolation from AuthFlow
|
||||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonExpectation, SinonMock, SinonStub } from 'sinon';
|
||||
import AbstractState from './AbstractState';
|
||||
import { AuthContext } from './AuthFlow';
|
||||
|
||||
export function bootstrap() {
|
||||
const context = {
|
||||
export interface MockedAuthContext extends AuthContext {
|
||||
getState: SinonStub;
|
||||
getRequest: SinonStub;
|
||||
}
|
||||
|
||||
export function bootstrap(): { context: MockedAuthContext; mock: SinonMock } {
|
||||
const context: MockedAuthContext = {
|
||||
getState: sinon.stub(),
|
||||
run() {},
|
||||
run() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
setState() {},
|
||||
getRequest: sinon.stub(),
|
||||
navigate() {},
|
||||
prevState: new (class State extends AbstractState {})(),
|
||||
};
|
||||
|
||||
const mock = sinon.mock(context);
|
||||
@@ -21,7 +31,10 @@ export function bootstrap() {
|
||||
return { context, mock };
|
||||
}
|
||||
|
||||
export function expectState(mock, state) {
|
||||
export function expectState(
|
||||
mock: SinonMock,
|
||||
state: typeof AbstractState,
|
||||
): SinonExpectation {
|
||||
return mock
|
||||
.expects('setState')
|
||||
.once()
|
||||
@@ -29,10 +42,10 @@ export function expectState(mock, state) {
|
||||
}
|
||||
|
||||
export function expectNavigate(
|
||||
mock,
|
||||
route,
|
||||
options: { [key: string]: any } | void,
|
||||
) {
|
||||
mock: SinonMock,
|
||||
route: string,
|
||||
options: Record<string, any> | void,
|
||||
): SinonExpectation {
|
||||
if (options) {
|
||||
return mock
|
||||
.expects('navigate')
|
||||
@@ -46,7 +59,10 @@ export function expectNavigate(
|
||||
.withExactArgs(route);
|
||||
}
|
||||
|
||||
export function expectRun(mock, ...args) {
|
||||
export function expectRun(
|
||||
mock: SinonMock,
|
||||
...args: Array<any>
|
||||
): SinonExpectation {
|
||||
return mock
|
||||
.expects('run')
|
||||
.once()
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
import AuthFlow, { ActionsDict, AuthContext as TAuthContext } from './AuthFlow';
|
||||
|
||||
const availableActions = {
|
||||
const availableActions: ActionsDict = {
|
||||
updateUser,
|
||||
authenticate,
|
||||
activateAccount,
|
||||
@@ -42,4 +42,4 @@ const availableActions = {
|
||||
};
|
||||
|
||||
export type AuthContext = TAuthContext;
|
||||
export default new AuthFlow(availableActions as ActionsDict);
|
||||
export default new AuthFlow(availableActions);
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import { RelativeTime } from 'app/components/ui';
|
||||
import React, { ComponentType, ReactElement, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { RelativeTime } from 'app/components/ui';
|
||||
|
||||
import messages from './errorsDict.intl.json';
|
||||
|
||||
/* eslint-disable react/prop-types, react/display-name, react/no-multi-comp, no-use-before-define */
|
||||
const SuggestResetPassword: ComponentType = () => (
|
||||
<>
|
||||
<br />
|
||||
<Message
|
||||
{...messages.suggestResetPassword}
|
||||
values={{
|
||||
forgotYourPassword: (
|
||||
<Link to="/forgot-password">
|
||||
<Message {...messages.forgotYourPassword} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export default {
|
||||
resolve(error) {
|
||||
let payload = {};
|
||||
const ResendKey: ComponentType<{ url: string }> = ({ url }) => (
|
||||
<>
|
||||
{' '}
|
||||
<Link to={url}>
|
||||
<Message {...messages.doYouWantRequestKey} />
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
if (error.type) {
|
||||
payload = error.payload || {};
|
||||
error = error.type;
|
||||
}
|
||||
|
||||
return errorsMap[error] ? errorsMap[error](payload) : error;
|
||||
},
|
||||
};
|
||||
|
||||
const errorsMap = {
|
||||
const errorsMap: Record<string, (props?: Record<string, any>) => ReactElement> = {
|
||||
'error.login_required': () => <Message {...messages.loginRequired} />,
|
||||
'error.login_not_exist': () => <Message {...messages.loginNotExist} />,
|
||||
'error.password_required': () => <Message {...messages.passwordRequired} />,
|
||||
|
||||
'error.password_incorrect': (props = {}) => (
|
||||
'error.password_incorrect': (props ) => (
|
||||
// props are handled in validationErrorsHandler in components/auth/actions
|
||||
<span>
|
||||
<>
|
||||
<Message {...messages.invalidPassword} />
|
||||
{props.isGuest ? errorsMap.suggestResetPassword() : null}
|
||||
</span>
|
||||
{props && props.isGuest ? <SuggestResetPassword /> : null}
|
||||
</>
|
||||
),
|
||||
|
||||
'error.username_required': () => <Message {...messages.usernameRequired} />,
|
||||
@@ -46,12 +56,12 @@ const errorsMap = {
|
||||
'error.email_too_long': () => <Message {...messages.emailToLong} />,
|
||||
'error.email_invalid': () => <Message {...messages.emailInvalid} />,
|
||||
'error.email_is_tempmail': () => <Message {...messages.emailIsTempmail} />,
|
||||
'error.email_not_available': (props = {}) => (
|
||||
'error.email_not_available': (props) => (
|
||||
// props are handled in validationErrorsHandler in components/auth/actions
|
||||
<span>
|
||||
<>
|
||||
<Message {...messages.emailNotAvailable} />
|
||||
{props.isGuest ? errorsMap.suggestResetPassword() : null}
|
||||
</span>
|
||||
{props && props.isGuest ? <SuggestResetPassword /> : null}
|
||||
</>
|
||||
),
|
||||
|
||||
'error.totp_required': () => <Message {...messages.totpRequired} />,
|
||||
@@ -72,10 +82,10 @@ const errorsMap = {
|
||||
),
|
||||
'error.key_required': () => <Message {...messages.keyRequired} />,
|
||||
'error.key_not_exists': props => (
|
||||
<span>
|
||||
<>
|
||||
<Message {...messages.keyNotExists} />
|
||||
{props.repeatUrl ? errorsMap.resendKey(props.repeatUrl) : null}
|
||||
</span>
|
||||
{props && props.repeatUrl ? <ResendKey url={props.repeatUrl} /> : null}
|
||||
</>
|
||||
),
|
||||
'error.key_expire': props => errorsMap['error.key_not_exists'](props),
|
||||
|
||||
@@ -96,7 +106,7 @@ const errorsMap = {
|
||||
{...messages.emailFrequency}
|
||||
values={{
|
||||
// for msLeft @see AuthError.jsx
|
||||
time: <RelativeTime timestamp={props.msLeft} />,
|
||||
time: <RelativeTime timestamp={props!.msLeft} />,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -107,7 +117,7 @@ const errorsMap = {
|
||||
),
|
||||
|
||||
'error.captcha_required': () => <Message {...messages.captchaRequired} />,
|
||||
'error.captcha_invalid': () => errorsMap['error.captcha_required'](),
|
||||
'error.captcha_invalid': props => errorsMap['error.captcha_required'](props),
|
||||
|
||||
'error.redirectUri_required': () => (
|
||||
<Message {...messages.redirectUriRequired} />
|
||||
@@ -115,29 +125,22 @@ const errorsMap = {
|
||||
'error.redirectUri_invalid': () => (
|
||||
<Message {...messages.redirectUriInvalid} />
|
||||
),
|
||||
|
||||
suggestResetPassword: () => (
|
||||
<span>
|
||||
<br />
|
||||
<Message
|
||||
{...messages.suggestResetPassword}
|
||||
values={{
|
||||
forgotYourPassword: (
|
||||
<Link to="/forgot-password">
|
||||
<Message {...messages.forgotYourPassword} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
|
||||
resendKey: url => (
|
||||
<span>
|
||||
{' '}
|
||||
<Link to={url}>
|
||||
<Message {...messages.doYouWantRequestKey} />
|
||||
</Link>
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
interface ErrorLiteral {
|
||||
type: string;
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
type Error = string | ErrorLiteral;
|
||||
|
||||
export function resolve(error: Error): ReactNode {
|
||||
let payload = {};
|
||||
|
||||
if (typeof error !== 'string') {
|
||||
payload = error.payload || {};
|
||||
error = error.type;
|
||||
}
|
||||
|
||||
return errorsMap[error] ? errorsMap[error](payload) : error;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import errorsDict from './errorsDict';
|
||||
|
||||
export default errorsDict;
|
||||
1
packages/app/services/errorsDict/index.ts
Normal file
1
packages/app/services/errorsDict/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolve } from './errorsDict';
|
||||
@@ -2,7 +2,7 @@
|
||||
* A helper wrapper service around window.history
|
||||
*/
|
||||
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { createBrowserHistory, History } from 'history';
|
||||
|
||||
export const browserHistory = createBrowserHistory();
|
||||
|
||||
@@ -10,7 +10,7 @@ browserHistory.listen(() => {
|
||||
patchHistory(browserHistory);
|
||||
});
|
||||
|
||||
function patchHistory(history) {
|
||||
function patchHistory(history: History): void {
|
||||
Object.assign(history.location, {
|
||||
query: new URLSearchParams(history.location.search),
|
||||
});
|
||||
@@ -19,6 +19,8 @@ function patchHistory(history) {
|
||||
patchHistory(browserHistory);
|
||||
|
||||
export default {
|
||||
initialLength: 0,
|
||||
|
||||
init() {
|
||||
this.initialLength = window.history.length;
|
||||
},
|
||||
@@ -1,29 +0,0 @@
|
||||
// On page initialization loader is already visible, so initial value is 1
|
||||
let stack = 1;
|
||||
|
||||
export default {
|
||||
show() {
|
||||
if (++stack >= 0) {
|
||||
const loader = document.getElementById('loader');
|
||||
|
||||
if (!loader) {
|
||||
throw new Error('Can not find loader element');
|
||||
}
|
||||
|
||||
loader.classList.add('is-active');
|
||||
}
|
||||
},
|
||||
|
||||
hide() {
|
||||
if (--stack <= 0) {
|
||||
stack = 0;
|
||||
const loader = document.getElementById('loader');
|
||||
|
||||
if (!loader) {
|
||||
throw new Error('Can not find loader element');
|
||||
}
|
||||
|
||||
loader.classList.remove('is-active', 'is-first-launch');
|
||||
}
|
||||
},
|
||||
};
|
||||
27
packages/app/services/loader.ts
Normal file
27
packages/app/services/loader.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// On page initialization loader is already visible, so initial value is 1
|
||||
let stack = 1;
|
||||
|
||||
export function show(): void {
|
||||
if (++stack >= 0) {
|
||||
const loader = document.getElementById('loader');
|
||||
|
||||
if (!loader) {
|
||||
throw new Error('Can not find loader element');
|
||||
}
|
||||
|
||||
loader.classList.add('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (--stack <= 0) {
|
||||
stack = 0;
|
||||
const loader = document.getElementById('loader');
|
||||
|
||||
if (!loader) {
|
||||
throw new Error('Can not find loader element');
|
||||
}
|
||||
|
||||
loader.classList.remove('is-active', 'is-first-launch');
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,15 @@ try {
|
||||
logger.info('No storage available'); // log for statistic purposes
|
||||
}
|
||||
|
||||
export function hasStorage() {
|
||||
export function hasStorage(): boolean {
|
||||
return _hasStorage;
|
||||
}
|
||||
|
||||
// TODO: work on
|
||||
class DummyStorage implements Storage {
|
||||
[name: string]: any;
|
||||
|
||||
readonly length: number;
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this[key] || null;
|
||||
}
|
||||
@@ -30,6 +33,14 @@ class DummyStorage implements Storage {
|
||||
removeItem(key: string): void {
|
||||
Reflect.deleteProperty(this, key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
Object.keys(this).forEach(this.removeItem);
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return Object.keys(this)[index] || null;
|
||||
}
|
||||
}
|
||||
|
||||
export const localStorage = _hasStorage
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
const STRING_MAX_LENGTH = 128 * 1024;
|
||||
|
||||
type Filter = (key: string | undefined, value: Record<string, any>) => any;
|
||||
|
||||
interface State {
|
||||
sizeLeft: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of any object without non-serializable elements to make result safe for JSON.stringify().
|
||||
* Guaranteed to never throw.
|
||||
@@ -16,14 +22,17 @@ const STRING_MAX_LENGTH = 128 * 1024;
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
function abbreviate(obj, options = {}) {
|
||||
const filter =
|
||||
options.filter ||
|
||||
function(key, value) {
|
||||
return value;
|
||||
};
|
||||
const maxDepth = options.depth || 10;
|
||||
const maxSize = options.maxSize || 1 * 1024 * 1024;
|
||||
function abbreviate(
|
||||
obj: Record<string, any>,
|
||||
options: {
|
||||
filter?: Filter;
|
||||
depth?: number;
|
||||
maxSize?: number;
|
||||
} = {},
|
||||
): string {
|
||||
const filter: Filter = options.filter || ((key, value) => value);
|
||||
const maxDepth: number = options.depth || 10;
|
||||
const maxSize: number = options.maxSize || 1 * 1024 * 1024;
|
||||
|
||||
return abbreviateRecursive(
|
||||
undefined,
|
||||
@@ -34,7 +43,7 @@ function abbreviate(obj, options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
function limitStringLength(str) {
|
||||
function limitStringLength(str: string): string {
|
||||
if (str.length > STRING_MAX_LENGTH) {
|
||||
return `${str.substring(0, STRING_MAX_LENGTH / 2)} … ${str.substring(
|
||||
str.length - STRING_MAX_LENGTH / 2,
|
||||
@@ -44,7 +53,14 @@ function limitStringLength(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function abbreviateRecursive(key, obj, filter, state, maxDepth) {
|
||||
function abbreviateRecursive(
|
||||
key: string | undefined,
|
||||
obj: any,
|
||||
filter: Filter,
|
||||
state: State,
|
||||
maxDepth: number,
|
||||
): any {
|
||||
// TODO: return type
|
||||
if (state.sizeLeft < 0) {
|
||||
return '**skipped**';
|
||||
}
|
||||
@@ -64,13 +80,16 @@ function abbreviateRecursive(key, obj, filter, state, maxDepth) {
|
||||
break; // fall back to stringification
|
||||
}
|
||||
|
||||
const newobj = Array.isArray(obj) ? [] : {};
|
||||
const newobj: Record<string, any> | Array<any> = Array.isArray(obj)
|
||||
? []
|
||||
: {};
|
||||
|
||||
for (const i in obj) {
|
||||
if (!obj.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
newobj[i] = abbreviateRecursive(
|
||||
i,
|
||||
obj[i],
|
||||
@@ -113,7 +132,7 @@ function abbreviateRecursive(key, obj, filter, state, maxDepth) {
|
||||
}
|
||||
}
|
||||
|
||||
function commonFilter(key, val) {
|
||||
function commonFilter(key: any, val: any): any {
|
||||
if (typeof val === 'function') {
|
||||
return undefined;
|
||||
}
|
||||
@@ -135,6 +154,7 @@ function commonFilter(key, val) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
err[i] = val[i];
|
||||
}
|
||||
|
||||
@@ -144,7 +164,7 @@ function commonFilter(key, val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
function nodeFilter(key, val) {
|
||||
function nodeFilter(key: any, val: any): any {
|
||||
// domain objects are huge and have circular references
|
||||
if (key === 'domain' && typeof val === 'object' && val._events) {
|
||||
return '**domain ignored**';
|
||||
@@ -161,7 +181,7 @@ function nodeFilter(key, val) {
|
||||
return commonFilter(key, val);
|
||||
}
|
||||
|
||||
function browserFilter(key, val) {
|
||||
function browserFilter(key: any, val: any): any {
|
||||
if (val === window) {
|
||||
return '**window**';
|
||||
}
|
||||
@@ -183,7 +203,7 @@ function browserFilter(key, val) {
|
||||
|
||||
export { abbreviate, nodeFilter, browserFilter };
|
||||
|
||||
export default function(obj) {
|
||||
export default function(obj: any): string {
|
||||
return abbreviate(obj, {
|
||||
filter: browserFilter,
|
||||
});
|
||||
@@ -86,7 +86,7 @@ class Logger {
|
||||
function log(
|
||||
level: 'error' | 'warning' | 'info' | 'debug',
|
||||
message: string | Error,
|
||||
context?: { [key: string]: any },
|
||||
context?: Record<string, any>,
|
||||
) {
|
||||
const method: 'error' | 'warn' | 'info' | 'debug' =
|
||||
level === 'warning' ? 'warn' : level;
|
||||
@@ -120,7 +120,7 @@ function log(
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function prepareContext(context: { [key: string]: any }) {
|
||||
function prepareContext(context: Record<string, any>): Promise<string> {
|
||||
if (context instanceof Response) {
|
||||
// TODO: rewrite abbreviate to use promises and recursively find Response
|
||||
return context
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Resp } from './request';
|
||||
|
||||
class InternalServerError extends Error {
|
||||
error: Error | { [key: string]: any };
|
||||
originalResponse: Resp<any>;
|
||||
export default class InternalServerError extends Error {
|
||||
public readonly error: Error | Record<string, any>;
|
||||
public readonly originalResponse: Resp<any>;
|
||||
|
||||
constructor(
|
||||
error: Error | string | { [key: string]: any },
|
||||
resp?: Response | { [key: string]: any },
|
||||
error: Error | string | Record<string, any>,
|
||||
resp?: Response | Record<string, any>,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -32,5 +32,3 @@ class InternalServerError extends Error {
|
||||
Object.assign(this, error);
|
||||
}
|
||||
}
|
||||
|
||||
export default InternalServerError;
|
||||
|
||||
@@ -5,23 +5,26 @@ import PromiseMiddlewareLayer from 'app/services/request/PromiseMiddlewareLayer'
|
||||
|
||||
describe('PromiseMiddlewareLayer', () => {
|
||||
describe('#add()', () => {
|
||||
let layer;
|
||||
let layer: PromiseMiddlewareLayer;
|
||||
|
||||
beforeEach(() => {
|
||||
layer = new PromiseMiddlewareLayer();
|
||||
});
|
||||
|
||||
it('should have no middlewares by default', () => {
|
||||
// @ts-ignore
|
||||
expect(layer.middlewares, 'to have length', 0);
|
||||
});
|
||||
|
||||
it('should add middleware into layer', () => {
|
||||
layer.add({});
|
||||
|
||||
// @ts-ignore
|
||||
expect(layer.middlewares, 'to have length', 1);
|
||||
});
|
||||
|
||||
it('throws if middleware is not object', () => {
|
||||
// @ts-ignore
|
||||
expect(() => layer.add(1), 'to throw', 'A middleware must be an object');
|
||||
});
|
||||
});
|
||||
@@ -36,7 +39,7 @@ describe('PromiseMiddlewareLayer', () => {
|
||||
testAction('then');
|
||||
testAction('catch');
|
||||
|
||||
function testAction(name) {
|
||||
function testAction(name: string) {
|
||||
describe(`run('${name}')`, () => {
|
||||
it('should run middleware', () => {
|
||||
const layer: any = new PromiseMiddlewareLayer();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Options } from './request';
|
||||
|
||||
type Action = 'catch' | 'then' | 'before';
|
||||
|
||||
interface MiddlewareRequestOptions {
|
||||
export interface MiddlewareRequestOptions {
|
||||
url: string;
|
||||
options: Options;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class PromiseMiddlewareLayer {
|
||||
options: MiddlewareRequestOptions,
|
||||
restart: () => Promise<any>,
|
||||
): Promise<any>;
|
||||
run(action: Action, data, ...rest) {
|
||||
run(action: Action, data: any, ...rest: any) {
|
||||
const promiseMethod = action === 'catch' ? 'catch' : 'then';
|
||||
|
||||
return this.middlewares
|
||||
@@ -81,18 +81,18 @@ class PromiseMiddlewareLayer {
|
||||
invoke(
|
||||
promise,
|
||||
promiseMethod,
|
||||
)(resp => invoke(middleware, action)(resp, ...rest)),
|
||||
)((resp: any) => invoke(middleware, action)(resp, ...rest)),
|
||||
invoke(Promise, action === 'catch' ? 'reject' : 'resolve')(data),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function invoke(instance: { [key: string]: any }, method: string) {
|
||||
function invoke(instance: Record<string, any>, method: string) {
|
||||
if (typeof instance[method] !== 'function') {
|
||||
throw new Error(`Can not invoke ${method} on ${instance}`);
|
||||
}
|
||||
|
||||
return (...args) => instance[method](...args);
|
||||
return (...args: any) => instance[method](...args);
|
||||
}
|
||||
|
||||
export default PromiseMiddlewareLayer;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
function RequestAbortedError(error: Error | Response) {
|
||||
this.name = 'RequestAbortedError';
|
||||
this.message = 'RequestAbortedError';
|
||||
this.error = error;
|
||||
this.stack = new Error().stack;
|
||||
export default class RequestAbortedError extends Error {
|
||||
private error: Error | Response;
|
||||
|
||||
constructor(error: Error | Response) {
|
||||
super();
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.message = this.constructor.name;
|
||||
this.stack = new Error().stack;
|
||||
|
||||
if (typeof error === 'string') {
|
||||
this.message = error;
|
||||
} else {
|
||||
if ('message' in error) {
|
||||
this.message = error.message;
|
||||
}
|
||||
@@ -15,8 +16,3 @@ function RequestAbortedError(error: Error | Response) {
|
||||
Object.assign(this, error);
|
||||
}
|
||||
}
|
||||
|
||||
RequestAbortedError.prototype = Object.create(Error.prototype);
|
||||
RequestAbortedError.prototype.constructor = RequestAbortedError;
|
||||
|
||||
export default RequestAbortedError;
|
||||
|
||||
@@ -19,11 +19,13 @@ describe('services/request', () => {
|
||||
|
||||
(fetch as any).returns(Promise.resolve(resp));
|
||||
|
||||
return expect(request.get('/foo'), 'to be rejected').then(error => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
expect(error.message, 'to contain', 'Unexpected token');
|
||||
});
|
||||
return expect(request.get('/foo'), 'to be rejected').then(
|
||||
(error: InternalServerError) => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
expect(error.message, 'to contain', 'Unexpected token');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap 5xx errors', () => {
|
||||
@@ -31,10 +33,12 @@ describe('services/request', () => {
|
||||
|
||||
(fetch as any).returns(Promise.resolve(resp));
|
||||
|
||||
return expect(request.get('/foo'), 'to be rejected').then(error => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
});
|
||||
return expect(request.get('/foo'), 'to be rejected').then(
|
||||
(error: InternalServerError) => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap aborted errors', () => {
|
||||
@@ -42,10 +46,13 @@ describe('services/request', () => {
|
||||
|
||||
(fetch as any).returns(Promise.resolve(resp));
|
||||
|
||||
return expect(request.get('/foo'), 'to be rejected').then(error => {
|
||||
expect(error, 'to be an', RequestAbortedError);
|
||||
expect(error.error, 'to be', resp);
|
||||
});
|
||||
return expect(request.get('/foo'), 'to be rejected').then(
|
||||
(error: RequestAbortedError) => {
|
||||
expect(error, 'to be an', RequestAbortedError);
|
||||
// @ts-ignore
|
||||
expect(error.error, 'to be', resp);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap "Failed to fetch" errors', () => {
|
||||
@@ -53,11 +60,14 @@ describe('services/request', () => {
|
||||
|
||||
(fetch as any).returns(Promise.resolve(resp));
|
||||
|
||||
return expect(request.get('/foo'), 'to be rejected').then(error => {
|
||||
expect(error, 'to be an', RequestAbortedError);
|
||||
expect(error.message, 'to be', resp.message);
|
||||
expect(error.error, 'to be', resp);
|
||||
});
|
||||
return expect(request.get('/foo'), 'to be rejected').then(
|
||||
(error: RequestAbortedError) => {
|
||||
expect(error, 'to be an', RequestAbortedError);
|
||||
expect(error.message, 'to be', resp.message);
|
||||
// @ts-ignore
|
||||
expect(error.error, 'to be', resp);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export type Resp<T> = T & {
|
||||
|
||||
export interface Options extends RequestInit {
|
||||
token?: string | null;
|
||||
headers: { [key: string]: any };
|
||||
headers: Record<string, any>;
|
||||
}
|
||||
|
||||
const buildOptions = (
|
||||
@@ -143,16 +143,16 @@ const rejectWithJSON = (resp: Response) =>
|
||||
|
||||
throw resp;
|
||||
});
|
||||
const handleResponseSuccess = resp =>
|
||||
const handleResponseSuccess = (resp: { success?: boolean }) =>
|
||||
resp.success || typeof resp.success === 'undefined'
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
|
||||
async function doFetch(url: string, options: Options) {
|
||||
async function doFetch<T>(url: string, options: Options): Promise<Resp<T>> {
|
||||
// NOTE: we are wrapping fetch, because it is returning
|
||||
// Promise instance that can not be polyfilled with Promise.prototype.finally
|
||||
|
||||
const headers: { [key: string]: string } = { ...options.headers } as any;
|
||||
const headers: Record<string, string> = { ...options.headers } as any;
|
||||
headers.Accept = 'application/json';
|
||||
|
||||
options.headers = headers;
|
||||
|
||||
Reference in New Issue
Block a user