mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-26 16:52:06 +05:30
Add RequestAbortedError and fix current e2e tests
This commit is contained in:
parent
a9d67bc3e7
commit
71bfc01e4d
@ -8,14 +8,16 @@ const ABORT_ERR = 20;
|
||||
|
||||
export default function BsodMiddleware(dispatchBsod: Function, logger: Logger) {
|
||||
return {
|
||||
catch<T: Resp<*> | InternalServerError>(resp?: T): Promise<T> {
|
||||
if (resp && (
|
||||
(resp instanceof InternalServerError
|
||||
&& resp.error.code !== ABORT_ERR
|
||||
) || (resp.originalResponse
|
||||
&& /5\d\d/.test((resp.originalResponse.status: string))
|
||||
)
|
||||
)) {
|
||||
catch<T: Resp<*> | InternalServerError | Error>(resp?: T): Promise<T> {
|
||||
const originalResponse: Object = (resp && resp.originalResponse) || {};
|
||||
|
||||
if (
|
||||
resp
|
||||
&& ((resp instanceof InternalServerError
|
||||
&& resp.error.code !== ABORT_ERR)
|
||||
|| (originalResponse
|
||||
&& /5\d\d/.test((originalResponse.status: string))))
|
||||
) {
|
||||
dispatchBsod();
|
||||
|
||||
if (!resp.message || !/NetworkError/.test(resp.message)) {
|
||||
@ -25,7 +27,7 @@ export default function BsodMiddleware(dispatchBsod: Function, logger: Logger) {
|
||||
message = `BSoD: ${resp.message}`;
|
||||
}
|
||||
|
||||
logger.warn(message, {resp});
|
||||
logger.warn(message, { resp });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
// @flow
|
||||
function InternalServerError(error: Error | string | Object, resp?: Response | Object) {
|
||||
function InternalServerError(
|
||||
error: Error | string | Object,
|
||||
resp?: Response | Object
|
||||
) {
|
||||
error = error || {};
|
||||
|
||||
this.name = 'InternalServerError';
|
||||
this.message = 'InternalServerError';
|
||||
this.error = error;
|
||||
this.stack = (new Error()).stack;
|
||||
this.stack = new Error().stack;
|
||||
|
||||
if (resp) {
|
||||
this.originalResponse = resp;
|
||||
|
23
src/services/request/RequestAbortedError.js
Normal file
23
src/services/request/RequestAbortedError.js
Normal file
@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
function RequestAbortedError(error: Error | Response) {
|
||||
this.name = 'RequestAbortedError';
|
||||
this.message = 'RequestAbortedError';
|
||||
this.error = error;
|
||||
this.stack = new Error().stack;
|
||||
|
||||
if (error.message) {
|
||||
this.message = error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
this.message = error;
|
||||
} else {
|
||||
this.error = error;
|
||||
Object.assign(this, error);
|
||||
}
|
||||
}
|
||||
|
||||
RequestAbortedError.prototype = Object.create(Error.prototype);
|
||||
RequestAbortedError.prototype.constructor = RequestAbortedError;
|
||||
|
||||
export default RequestAbortedError;
|
@ -2,6 +2,7 @@
|
||||
export { default } from './request';
|
||||
export type { Resp } from './request';
|
||||
export { default as InternalServerError } from './InternalServerError';
|
||||
export { default as RequestAbortedError } from './RequestAbortedError';
|
||||
|
||||
/**
|
||||
* Usage: Query<'requeired'|'keys'|'names'>
|
||||
|
@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import PromiseMiddlewareLayer from './PromiseMiddlewareLayer';
|
||||
import InternalServerError from './InternalServerError';
|
||||
import RequestAbortedError from './RequestAbortedError';
|
||||
|
||||
const middlewareLayer = new PromiseMiddlewareLayer();
|
||||
|
||||
@ -17,10 +18,10 @@ type Middleware = {
|
||||
const buildOptions = (method: string, data?: Object, options: Object) => ({
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: buildQuery(data),
|
||||
...options,
|
||||
...options
|
||||
});
|
||||
|
||||
export default {
|
||||
@ -31,7 +32,11 @@ export default {
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
post<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
||||
post<T>(
|
||||
url: string,
|
||||
data?: Object,
|
||||
options: Object = {}
|
||||
): Promise<Resp<T>> {
|
||||
return doFetch(url, buildOptions('POST', data, options));
|
||||
},
|
||||
|
||||
@ -55,7 +60,11 @@ export default {
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
delete<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
||||
delete<T>(
|
||||
url: string,
|
||||
data?: Object,
|
||||
options: Object = {}
|
||||
): Promise<Resp<T>> {
|
||||
return doFetch(url, buildOptions('DELETE', data, options));
|
||||
},
|
||||
|
||||
@ -93,36 +102,40 @@ export default {
|
||||
*/
|
||||
addMiddleware(middleware: Middleware) {
|
||||
middlewareLayer.add(middleware);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const checkStatus = (resp: Response) => resp.status >= 200 && resp.status < 300
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
const toJSON = (resp = {}) => {
|
||||
if (!resp.json) {
|
||||
// e.g. 'TypeError: Failed to fetch' due to CORS
|
||||
throw new InternalServerError(resp);
|
||||
const checkStatus = (resp: Response) =>
|
||||
resp.status >= 200 && resp.status < 300
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
const toJSON = (resp: Response) => {
|
||||
if (!resp.json || resp.status === 0) {
|
||||
// e.g. 'TypeError: Failed to fetch' due to CORS or request was aborted
|
||||
throw new RequestAbortedError(resp);
|
||||
}
|
||||
|
||||
return resp.json().then((json) => {
|
||||
json.originalResponse = resp;
|
||||
return resp.json().then(
|
||||
(json) => {
|
||||
json.originalResponse = resp;
|
||||
|
||||
return json;
|
||||
}, (error) => Promise.reject(
|
||||
new InternalServerError(error, resp)
|
||||
));
|
||||
return json;
|
||||
},
|
||||
(error) => Promise.reject(new InternalServerError(error, resp))
|
||||
);
|
||||
};
|
||||
const rejectWithJSON = (resp) => toJSON(resp).then((resp) => {
|
||||
if (resp.originalResponse.status >= 500) {
|
||||
throw new InternalServerError(resp, resp.originalResponse);
|
||||
}
|
||||
const rejectWithJSON = (resp: Response) =>
|
||||
toJSON(resp).then((resp) => {
|
||||
if (resp.originalResponse.status >= 500) {
|
||||
throw new InternalServerError(resp, resp.originalResponse);
|
||||
}
|
||||
|
||||
throw resp;
|
||||
});
|
||||
const handleResponseSuccess = (resp) => resp.success || typeof resp.success === 'undefined'
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
throw resp;
|
||||
});
|
||||
const handleResponseSuccess = (resp) =>
|
||||
resp.success || typeof resp.success === 'undefined'
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
|
||||
async function doFetch(url, options = {}) {
|
||||
// NOTE: we are wrapping fetch, because it is returning
|
||||
@ -131,14 +144,21 @@ async function doFetch(url, options = {}) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers.Accept = 'application/json';
|
||||
|
||||
return middlewareLayer.run('before', {url, options})
|
||||
.then(({url, options}) =>
|
||||
return middlewareLayer
|
||||
.run('before', { url, options })
|
||||
.then(({ url, options }) =>
|
||||
fetch(url, options)
|
||||
.then(checkStatus)
|
||||
.then(toJSON, rejectWithJSON)
|
||||
.then(handleResponseSuccess)
|
||||
.then((resp) => middlewareLayer.run('then', resp, {url, options}))
|
||||
.catch((resp) => middlewareLayer.run('catch', resp, {url, options}, () => doFetch(url, options)))
|
||||
.then((resp) =>
|
||||
middlewareLayer.run('then', resp, { url, options })
|
||||
)
|
||||
.catch((resp) =>
|
||||
middlewareLayer.run('catch', resp, { url, options }, () =>
|
||||
doFetch(url, options)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -174,11 +194,10 @@ function convertQueryValue(value) {
|
||||
*/
|
||||
function buildQuery(data: Object = {}): string {
|
||||
return Object.keys(data)
|
||||
.map(
|
||||
(keyName) =>
|
||||
[keyName, convertQueryValue(data[keyName])]
|
||||
.map(encodeURIComponent)
|
||||
.join('=')
|
||||
.map((keyName) =>
|
||||
[keyName, convertQueryValue(data[keyName])]
|
||||
.map(encodeURIComponent)
|
||||
.join('=')
|
||||
)
|
||||
.join('&');
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import expect from 'unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import request from 'services/request';
|
||||
import { InternalServerError } from 'services/request';
|
||||
import { InternalServerError, RequestAbortedError } from 'services/request';
|
||||
|
||||
describe('services/request', () => {
|
||||
beforeEach(() => {
|
||||
@ -14,42 +14,50 @@ describe('services/request', () => {
|
||||
});
|
||||
|
||||
describe('InternalServerError', () => {
|
||||
it('should wrap fetch error', () => {
|
||||
const resp = new TypeError('Fetch error');
|
||||
|
||||
fetch.returns(Promise.reject(resp));
|
||||
|
||||
return expect(request.get('/foo'), 'to be rejected')
|
||||
.then((error) => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be undefined');
|
||||
expect(error.message, 'to equal', resp.message);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap json errors', () => {
|
||||
const resp = new Response('bad resp format', {status: 200});
|
||||
const resp = new Response('bad resp format', { status: 200 });
|
||||
|
||||
fetch.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) => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
expect(error.message, 'to contain', 'Unexpected token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap 5xx errors', () => {
|
||||
const resp = new Response('{}', {status: 500});
|
||||
const resp = new Response('{}', { status: 500 });
|
||||
|
||||
fetch.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) => {
|
||||
expect(error, 'to be an', InternalServerError);
|
||||
expect(error.originalResponse, 'to be', resp);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap aborted errors', () => {
|
||||
const resp = new Response('{}', { status: 0 });
|
||||
|
||||
fetch.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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap "Failed to fetch" errors', () => {
|
||||
const resp = new TypeError('Failed to fetch');
|
||||
|
||||
fetch.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -63,7 +71,8 @@ describe('services/request', () => {
|
||||
positive: true,
|
||||
negative: false
|
||||
};
|
||||
const expectedQs = 'notSet=¬Set2=&numeric=1&complexString=sdfgs%20sdfg%20&positive=1&negative=0';
|
||||
const expectedQs
|
||||
= 'notSet=¬Set2=&numeric=1&complexString=sdfgs%20sdfg%20&positive=1&negative=0';
|
||||
|
||||
expect(request.buildQuery(data), 'to equal', expectedQs);
|
||||
});
|
||||
|
@ -33,8 +33,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
);
|
||||
|
||||
beforeEach(() =>
|
||||
localStorage.setItem('redux-storage', JSON.stringify(multiAccount))
|
||||
);
|
||||
localStorage.setItem('redux-storage', JSON.stringify(multiAccount)));
|
||||
|
||||
it('should ask for password', () => {
|
||||
cy.visit('/');
|
||||
@ -52,8 +51,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
|
||||
cy.url().should('include', '/password');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-toolbar] a')
|
||||
cy.get('[data-e2e-toolbar] a')
|
||||
.contains('Ely.by')
|
||||
.click();
|
||||
|
||||
@ -70,13 +68,11 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
|
||||
cy.url().should('include', '/choose-account');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-content]')
|
||||
cy.get('[data-e2e-content]')
|
||||
.contains(account2.email)
|
||||
.should('not.exist');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-content]')
|
||||
cy.get('[data-e2e-content]')
|
||||
.contains(account1.username)
|
||||
.click();
|
||||
|
||||
@ -85,14 +81,9 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
});
|
||||
|
||||
it('it should redirect to login, when one account and clicking back', () => {
|
||||
cy
|
||||
.url()
|
||||
.should(() =>
|
||||
localStorage.setItem(
|
||||
'redux-storage',
|
||||
JSON.stringify(singleAccount)
|
||||
)
|
||||
);
|
||||
cy.url().should(() =>
|
||||
localStorage.setItem('redux-storage', JSON.stringify(singleAccount))
|
||||
);
|
||||
cy.visit('/');
|
||||
|
||||
cy.url().should('include', '/password');
|
||||
@ -107,24 +98,26 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
it('should allow logout', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-toolbar]')
|
||||
cy.get('@fetch', { timeout: 15000 }).should(
|
||||
'be.calledWith',
|
||||
'/api/accounts/current'
|
||||
);
|
||||
|
||||
cy.get('[data-e2e-toolbar]')
|
||||
.contains(account2.username)
|
||||
.click();
|
||||
cy
|
||||
.get('[data-e2e-toolbar]')
|
||||
cy.get('[data-e2e-toolbar]')
|
||||
.contains('Log out')
|
||||
.click();
|
||||
|
||||
cy
|
||||
.get('@fetch', { timeout: 15000 })
|
||||
.should('be.calledWith', '/api/authentication/logout');
|
||||
cy
|
||||
.get('[data-e2e-toolbar]')
|
||||
cy.get('@fetch', { timeout: 15000 }).should(
|
||||
'be.calledWith',
|
||||
'/api/authentication/logout'
|
||||
);
|
||||
cy.get('[data-e2e-toolbar]')
|
||||
.contains(account2.email)
|
||||
.should('not.exist');
|
||||
cy
|
||||
.get('[data-e2e-toolbar]')
|
||||
cy.get('[data-e2e-toolbar]')
|
||||
.contains(account2.username)
|
||||
.should('not.exist');
|
||||
});
|
||||
@ -132,6 +125,11 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
it('should allow enter new login from choose account', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('@fetch', { timeout: 15000 }).should(
|
||||
'be.calledWith',
|
||||
'/api/accounts/current'
|
||||
);
|
||||
|
||||
cy.url().should('include', '/password');
|
||||
|
||||
cy.get('[data-e2e-go-back]').click();
|
||||
@ -169,22 +167,19 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
});
|
||||
|
||||
it('should ask for password if selected account with bad token', () => {
|
||||
cy
|
||||
.url()
|
||||
.should(() =>
|
||||
localStorage.setItem(
|
||||
'redux-storage',
|
||||
JSON.stringify(multiAccountWithBadTokens)
|
||||
)
|
||||
);
|
||||
cy.url().should(() =>
|
||||
localStorage.setItem(
|
||||
'redux-storage',
|
||||
JSON.stringify(multiAccountWithBadTokens)
|
||||
)
|
||||
);
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('[data-e2e-go-back]').click();
|
||||
|
||||
cy.url().should('include', '/choose-account');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-content]')
|
||||
cy.get('[data-e2e-content]')
|
||||
.contains(account1.username)
|
||||
.click();
|
||||
|
||||
@ -213,7 +208,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
*
|
||||
* @see https://trello.com/c/iINbZ2l2
|
||||
*/
|
||||
it('should allow enter register page', () => {
|
||||
it('should allow enter register page during password request for other account with invalid token', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.url().should('contain', '/password');
|
||||
@ -223,8 +218,6 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
cy.contains('[type=submit]', 'Log into another account').click();
|
||||
cy.contains('a', 'Create new account').click();
|
||||
|
||||
cy.get('@fetch').should('be.calledWith', '/api/options');
|
||||
|
||||
cy.url().should('contain', '/register');
|
||||
});
|
||||
|
||||
@ -241,11 +234,9 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
*
|
||||
* @see https://trello.com/c/iINbZ2l2
|
||||
*/
|
||||
it('should allow enter register page', () => {
|
||||
it('should allow enter register page, when current account has invalid token', () => {
|
||||
cy.visit('/register');
|
||||
|
||||
cy.get('@fetch').should('be.calledWith', '/api/options');
|
||||
|
||||
cy.url().should('contain', '/register');
|
||||
});
|
||||
|
||||
@ -260,8 +251,7 @@ describe('when user\'s token and refreshToken are invalid', () => {
|
||||
|
||||
cy.url({ timeout: 15000 }).should('contain', '/oauth/choose-account');
|
||||
|
||||
cy
|
||||
.get('[data-e2e-content]')
|
||||
cy.get('[data-e2e-content]')
|
||||
.contains(account2.username)
|
||||
.click();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user