Skip to content

Commit

Permalink
feat(backend): use helper from @eclipse-che/common instead of NodeReq…
Browse files Browse the repository at this point in the history
…uestError
  • Loading branch information
akurinnoy committed Sep 8, 2021
1 parent 66e658a commit fda8e7c
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 135 deletions.
1 change: 1 addition & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test": "jest"
},
"devDependencies": {
"@kubernetes/client-node": "^0.15.1",
"@types/jest": "^25.2.3",
"axios": "^0.21.1",
"axios-mock-adapter": "^1.20.0",
Expand Down
200 changes: 136 additions & 64 deletions packages/common/src/helpers/__tests__/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
*/

import axios, { AxiosError, AxiosResponse } from 'axios';
import { HttpError } from '@kubernetes/client-node';
import AxiosMockAdapter from 'axios-mock-adapter';
import * as http from 'http';
import { getMessage } from '../errors';

let mockAxios = new AxiosMockAdapter(axios);
Expand Down Expand Up @@ -42,90 +44,160 @@ describe('Errors helper', () => {
const expectedMessage = 'Unexpected error. Check DevTools console and network tabs for more information.'
expect(getMessage(notError)).toEqual(expectedMessage);

const expectedOutput = ['Unexpected error:', {'alert': 'Beware of bugs!'}];
const expectedOutput = ['Unexpected error:', { 'alert': 'Beware of bugs!' }];
expect(console.error).toBeCalledWith(...expectedOutput);
})

it('should return error message if server responds with error', async (done) => {
const message = '500 Internal Server Error.';
describe('Frontend errors', () => {

mockAxios.onGet('/location/not/found').replyOnce(() => {
return [500, {}, {},]

it('should return error message if server responds with error', async (done) => {
const message = '"500 Internal Server Error" returned by "/location/".';

mockAxios.onGet('/location/').replyOnce(() => {
return [500, {}, {},]
});

try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
const err = e as AxiosError;
// provide `statusText` to the response because mocking library cannot do that
(err.response as AxiosResponse<unknown>).statusText = 'Internal Server Error';

expect(getMessage(err)).toEqual(message);
done();
}
});

try {
const data = await axios.get('/location/not/found');
done.fail();
} catch (e) {
const err = e as AxiosError;
// provide `statusText` to the response because mocking library cannot do that
(err.response as AxiosResponse<unknown>).statusText = 'Internal Server Error';

expect(getMessage(err)).toEqual(message);
done();
}
});
it('should return error message if server responds with error', async (done) => {
const message = 'The server failed to fulfill a request';

it('should return error message if server responds with error', async (done) => {
const message = 'The server failed to fulfill a request';
mockAxios.onGet('/location/').replyOnce(() => {
return [500, { message }, {},]
});

mockAxios.onGet('/location/not/found').replyOnce(() => {
return [500, {message}, {},]
try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
const err = e as AxiosError;
// provide `statusText` to the response because mocking library cannot do that
(err.response as AxiosResponse<unknown>).statusText = 'Internal Server Error';

expect(getMessage(err)).toEqual(message);
done();
}
});

try {
const data = await axios.get('/location/not/found');
done.fail();
} catch (e) {
const err = e as AxiosError;
// provide `statusText` to the response because mocking library cannot do that
(err.response as AxiosResponse<unknown>).statusText = 'Internal Server Error';

expect(getMessage(err)).toEqual(message);
done();
}
});
it('should return error message if network error', async (done) => {
const message = 'Network Error';

it('should return error message if network error', async (done) => {
const message = 'Network Error';
mockAxios.onGet('/location/').networkErrorOnce();

mockAxios.onGet('/location/').networkErrorOnce();
try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});

try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});
it('should return error message if network timeout', async (done) => {
const message = 'timeout of 0ms exceeded';

mockAxios.onGet('/location/').timeoutOnce();

it('should return error message if network timeout', async (done) => {
const message = 'timeout of 0ms exceeded';
try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});

it('should return error message if request aborted', async (done) => {
const message = 'Request aborted';

mockAxios.onGet('/location/').timeoutOnce();
mockAxios.onGet('/location/').abortRequestOnce();

try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});

try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});

it('should return error message if request aborted', async (done) => {
const message = 'Request aborted';
describe('Backend errors', () => {

it('should return error message if no response available', () => {
const error: HttpError = {
name: 'HttpError',
message: 'No response available.',
response: {
url: '/location/'
} as http.IncomingMessage,
body: 'No response available',
statusCode: -1,
};
const expectedMessage = 'no response available due to network issue.';
expect(getMessage(error)).toEqual(expectedMessage);

delete error.statusCode;
expect(getMessage(error)).toEqual(expectedMessage);
});

it('should return error message if message from K8s is present', () => {
const expectedMessage = 'Error message from K8s.'
const error: HttpError = {
name: 'HttpError',
message: expectedMessage,
response: {
url: '/location/'
} as http.IncomingMessage,
body: {
message: expectedMessage,
},
statusCode: 500,
};
expect(getMessage(error)).toEqual(expectedMessage);
});

mockAxios.onGet('/location/').abortRequestOnce();
it('should return error message if message in response body is present', () => {
const expectedMessage = 'Error message from K8s.'
const error: HttpError = {
name: 'HttpError',
message: expectedMessage,
response: {
url: '/location/'
} as http.IncomingMessage,
body: expectedMessage,
statusCode: 500,
};
expect(getMessage(error)).toEqual(expectedMessage);
});

it('should return error message if `statusCode` is present', () => {
const expectedMessage = '"500" returned by "/location/".'
const error: HttpError = {
name: 'HttpError',
message: expectedMessage,
response: {
url: '/location/'
} as http.IncomingMessage,
body: undefined,
statusCode: 500,
};
expect(getMessage(error)).toEqual(expectedMessage);
});

try {
const data = await axios.get('/location/');
done.fail();
} catch (e) {
expect(getMessage(e)).toEqual(message);
done();
}
});

});
Expand Down
27 changes: 26 additions & 1 deletion packages/common/src/helpers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { AxiosError, AxiosResponse } from 'axios';
import { HttpError } from "@kubernetes/client-node";

/**
* This helper function does its best to get an error message from the provided object.
Expand All @@ -22,12 +23,30 @@ export function getMessage(error: unknown): string {
return 'Unexpected error.';
}

if (isKubeClientError(error)) {
let statusCode = error.statusCode || error.response.statusCode;
if (!statusCode || statusCode === -1) {
return 'no response available due to network issue.';
}
if (error.body?.message) {
// body is from K8s in JSON form with message present
return error.body.message;
}
if (error.body) {
// pure http response body without message available
return error.body;
}
return `"${statusCode}" returned by "${error.response.url}".`;
}

if (isAxiosError(error) && isAxiosResponse(error.response)) {
const response = error.response;
if (response.data.message) {
return response.data.message;
} else if (response.config.url) {
return `"${response.status} ${response.statusText}" returned by "${response.config.url}".`;
} else {
return `${response.status} ${response.statusText}.`;
return `"${response.status} ${response.statusText}".`;
}
}

Expand Down Expand Up @@ -63,3 +82,9 @@ export function isAxiosError(object: unknown): object is AxiosError {
return object !== undefined
&& (object as AxiosError).isAxiosError === true;
}

export function isKubeClientError(error: unknown): error is HttpError {
return isError(error)
&& (error as HttpError).response !== undefined
&& 'body' in (error as HttpError);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { conditionalTest, isIntegrationTestEnabled } from './utils/suite';
import { createKubeConfig } from './utils/helper';
import { fail } from 'assert';
import * as k8s from '@kubernetes/client-node';
import { NodeRequestError } from '../errors';
import { HttpError } from '@kubernetes/client-node';
import { helpers } from '@eclipse-che/common';

describe('Kubernetes API integration testing against cluster', () => {

Expand All @@ -33,8 +32,8 @@ describe('Kubernetes API integration testing against cluster', () => {
);
fail('request to non-existing Custom API should fail');
} catch (e) {
let error = new NodeRequestError('unable get non-existing', (e as HttpError));
expect(error.message).toBe('unable get non-existing: 404 page not found\n');
let errorMessage = 'unable get non-existing: ' + helpers.errors.getMessage(e);
expect(errorMessage).toBe('unable get non-existing: 404 page not found\n');
}
done();
}, 1000);
Expand Down
43 changes: 0 additions & 43 deletions packages/dashboard-backend/src/devworkspace-client/errors.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { ICheApi, } from '../../types';
import { projectApiGroup, projectRequestResources, projectResources, } from '../../const';
import { namespaceModel, projectRequestModel } from '../../const/models';
import { findApi } from '../helpers';
import { NodeRequestError } from '../../errors';
import { HttpError } from '@kubernetes/client-node';
import { helpers } from '@eclipse-che/common';

/**
* @deprecated Che Server started to provide rest endpoint to get namespace prepared.
Expand Down Expand Up @@ -48,7 +47,7 @@ export class CheApi implements ICheApi {
}
}
} catch (e) {
return Promise.reject(new NodeRequestError('unable to init project', (e as HttpError)));
throw new Error('unable to init project: ' + helpers.errors.getMessage(e));
}
}

Expand Down Expand Up @@ -92,7 +91,7 @@ export class CheApi implements ICheApi {
projectRequestModel(namespace)
);
} catch (e) {
return Promise.reject(new NodeRequestError('unable to create project', (e as HttpError)));
throw new Error('unable to create project: ' + helpers.errors.getMessage(e));
}
}

Expand All @@ -102,7 +101,7 @@ export class CheApi implements ICheApi {
namespaceModel(namespace)
);
} catch (e) {
return Promise.reject(new NodeRequestError('unable to create namespace', (e as HttpError)));
throw new Error('unable to create namespace: ' + helpers.errors.getMessage(e));
}
}
}
Loading

0 comments on commit fda8e7c

Please sign in to comment.