Implemented strict mode for the project (broken tests, hundreds of @ts-ignore and new errors are included) [skip ci]

This commit is contained in:
ErickSkrauch
2020-01-17 23:37:52 +03:00
committed by SleepWalker
parent 10e8b77acf
commit 96049ad4ad
151 changed files with 2470 additions and 1869 deletions

View File

@@ -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 },
]);
});

View File

@@ -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),

View File

@@ -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);
},

View File

@@ -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(() => {

View File

@@ -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 });
}

View File

@@ -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 {}
}

View File

@@ -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();

View File

@@ -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');
}
}

View File

@@ -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));
});

View File

@@ -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());
}
}

View File

@@ -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,
}),

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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');
}
}

View File

@@ -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({

View File

@@ -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;

View File

@@ -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();

View File

@@ -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');
}
}

View File

@@ -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();

View File

@@ -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());
}
}

View File

@@ -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));
});

View File

@@ -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: '/',
});

View File

@@ -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();

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -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();

View File

@@ -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,

View File

@@ -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));
});

View File

@@ -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');
}
}

View File

@@ -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));
});

View File

@@ -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 {

View File

@@ -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));
});

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -1,3 +0,0 @@
import errorsDict from './errorsDict';
export default errorsDict;

View File

@@ -0,0 +1 @@
export { resolve } from './errorsDict';

View File

@@ -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;
},

View File

@@ -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');
}
},
};

View 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');
}
}

View File

@@ -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

View File

@@ -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,
});

View File

@@ -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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
},
);
});
});

View File

@@ -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;