[Connectors] Check connector's responses (#115797)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2021-10-25 10:34:36 +03:00 committed by GitHub
parent 110a8418f9
commit 4d3644030a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1055 additions and 605 deletions

View file

@ -27,7 +27,7 @@ import {
} from './types';
import * as i18n from './translations';
import { request, getErrorMessage } from '../lib/axios_utils';
import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils';
import { ActionsConfigurationUtilities } from '../../actions_config';
const VERSION = '2';
@ -111,19 +111,15 @@ export const createExternalService = (
.filter((item) => !isEmpty(item))
.join(', ');
const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => {
const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => {
if (errorResponse == null) {
return '';
}
if (typeof errorResponse === 'string') {
// Jira error.response.data can be string!!
return errorResponse;
return 'unknown: errorResponse was null';
}
const { errorMessages, errors } = errorResponse;
if (errors == null) {
return '';
return 'unknown: errorResponse.errors was null';
}
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
@ -185,9 +181,14 @@ export const createExternalService = (
configurationUtilities,
});
const { fields, ...rest } = res.data;
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id', 'key'],
});
return { ...rest, ...fields };
const { fields, id: incidentId, key } = res.data;
return { id: incidentId, key, created: fields.created, updated: fields.updated, ...fields };
} catch (error) {
throw new Error(
getErrorMessage(
@ -234,6 +235,11 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id'],
});
const updatedIncident = await getIncident(res.data.id);
return {
@ -266,7 +272,7 @@ export const createExternalService = (
const fields = createFields(projectKey, incidentWithoutNullValues);
try {
await request({
const res = await request({
axios: axiosInstance,
method: 'put',
url: `${incidentUrl}/${incidentId}`,
@ -275,6 +281,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const updatedIncident = await getIncident(incidentId as string);
return {
@ -309,6 +319,11 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id', 'created'],
});
return {
commentId: comment.commentId,
externalCommentId: res.data.id,
@ -336,6 +351,11 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['capabilities'],
});
return { ...res.data };
} catch (error) {
throw new Error(
@ -362,6 +382,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const issueTypes = res.data.projects[0]?.issuetypes ?? [];
return normalizeIssueTypes(issueTypes);
} else {
@ -373,6 +397,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const issueTypes = res.data.values;
return normalizeIssueTypes(issueTypes);
}
@ -401,6 +429,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const fields = res.data.projects[0]?.issuetypes[0]?.fields || {};
return normalizeFields(fields);
} else {
@ -412,6 +444,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const fields = res.data.values.reduce(
(acc: { [x: string]: {} }, value: { fieldId: string }) => ({
...acc,
@ -471,6 +507,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
return normalizeSearchResults(res.data?.issues ?? []);
} catch (error) {
throw new Error(
@ -495,6 +535,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
return normalizeIssue(res.data ?? {});
} catch (error) {
throw new Error(

View file

@ -10,7 +10,14 @@ import { Agent as HttpsAgent } from 'https';
import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { Logger } from '../../../../../../src/core/server';
import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils';
import {
addTimeZoneToDate,
request,
patch,
getErrorMessage,
throwIfResponseIsNotValid,
createAxiosResponse,
} from './axios_utils';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
import { getCustomAgents } from './get_custom_agents';
@ -292,3 +299,82 @@ describe('getErrorMessage', () => {
expect(msg).toBe('[Action][My connector name]: An error has occurred');
});
});
describe('throwIfResponseIsNotValid', () => {
const res = createAxiosResponse({
headers: { ['content-type']: 'application/json' },
data: { incident: { id: '1' } },
});
test('it does NOT throw if the request is valid', () => {
expect(() => throwIfResponseIsNotValid({ res })).not.toThrow();
});
test('it does throw if the content-type is not json', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, headers: { ['content-type']: 'text/html' } },
})
).toThrow(
'Unsupported content type: text/html in GET https://example.com. Supported content types: application/json'
);
});
test('it does throw if the content-type is undefined', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, headers: {} },
})
).toThrow(
'Unsupported content type: undefined in GET https://example.com. Supported content types: application/json'
);
});
test('it does throw if the data is not an object or array', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, data: 'string' },
})
).toThrow('Response is not a valid JSON');
});
test('it does NOT throw if the data is an array', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, data: ['test'] },
})
).not.toThrow();
});
test.each(['', [], {}])('it does NOT throw if the data is %p', (data) => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, data },
})
).not.toThrow();
});
test('it does throw if the required attribute is not in the response', () => {
expect(() =>
throwIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: ['not-exist'] })
).toThrow('Response is missing at least one of the expected fields: not-exist');
});
test('it does throw if the required attribute are defined and the data is an array', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, data: ['test'] },
requiredAttributesToBeInTheResponse: ['not-exist'],
})
).toThrow('Response is missing at least one of the expected fields: not-exist');
});
test('it does NOT throw if the value of the required attribute is null', () => {
expect(() =>
throwIfResponseIsNotValid({
res: { ...res, data: { id: null } },
requiredAttributesToBeInTheResponse: ['id'],
})
).not.toThrow();
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { isObjectLike, isEmpty } from 'lodash';
import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios';
import { Logger } from '../../../../../../src/core/server';
import { getCustomAgents } from './get_custom_agents';
@ -76,3 +77,70 @@ export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => {
export const getErrorMessage = (connector: string, msg: string) => {
return `[Action][${connector}]: ${msg}`;
};
export const throwIfResponseIsNotValid = ({
res,
requiredAttributesToBeInTheResponse = [],
}: {
res: AxiosResponse;
requiredAttributesToBeInTheResponse?: string[];
}) => {
const requiredContentType = 'application/json';
const contentType = res.headers['content-type'] ?? 'undefined';
const data = res.data;
/**
* Check that the content-type of the response is application/json.
* Then includes is added because the header can be application/json;charset=UTF-8.
*/
if (!contentType.includes(requiredContentType)) {
throw new Error(
`Unsupported content type: ${contentType} in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}`
);
}
/**
* Check if the response is a JS object (data != null && typeof data === 'object')
* in case the content type is application/json but for some reason the response is not.
* Empty responses (204 No content) are ignored because the typeof data will be string and
* isObjectLike will fail.
* Axios converts automatically JSON to JS objects.
*/
if (!isEmpty(data) && !isObjectLike(data)) {
throw new Error('Response is not a valid JSON');
}
if (requiredAttributesToBeInTheResponse.length > 0) {
const requiredAttributesError = new Error(
`Response is missing at least one of the expected fields: ${requiredAttributesToBeInTheResponse.join(
','
)}`
);
/**
* If the response is an array and requiredAttributesToBeInTheResponse
* are not empty then we thrown an error assuming that the consumer
* expects an object response and not an array.
*/
if (Array.isArray(data)) {
throw requiredAttributesError;
}
requiredAttributesToBeInTheResponse.forEach((attr) => {
// Check only for undefined as null is a valid value
if (data[attr] === undefined) {
throw requiredAttributesError;
}
});
}
};
export const createAxiosResponse = (res: Partial<AxiosResponse>): AxiosResponse => ({
data: {},
status: 200,
statusText: 'OK',
headers: { ['content-type']: 'application/json' },
config: { method: 'GET', url: 'https://example.com' },
...res,
});

View file

@ -8,7 +8,7 @@
import axios from 'axios';
import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
import * as utils from '../lib/axios_utils';
import { request, createAxiosResponse } from '../lib/axios_utils';
import { ExternalService } from './types';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => {
});
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
const requestMock = request as jest.Mock;
const now = Date.now;
const TIMESTAMP = 1589391874472;
const configurationUtilities = actionsConfigMock.create();
@ -38,44 +38,50 @@ const configurationUtilities = actionsConfigMock.create();
// b) Update the incident
// c) Get the updated incident
const mockIncidentUpdate = (withUpdateError = false) => {
requestMock.mockImplementationOnce(() => ({
data: {
id: '1',
name: 'title',
description: {
format: 'html',
content: 'description',
requestMock.mockImplementationOnce(() =>
createAxiosResponse({
data: {
id: '1',
name: 'title',
description: {
format: 'html',
content: 'description',
},
incident_type_ids: [1001, 16, 12],
severity_code: 6,
},
incident_type_ids: [1001, 16, 12],
severity_code: 6,
},
}));
})
);
if (withUpdateError) {
requestMock.mockImplementationOnce(() => {
throw new Error('An error has occurred');
});
} else {
requestMock.mockImplementationOnce(() => ({
data: {
success: true,
id: '1',
inc_last_modified_date: 1589391874472,
},
}));
requestMock.mockImplementationOnce(() =>
createAxiosResponse({
data: {
success: true,
id: '1',
inc_last_modified_date: 1589391874472,
},
})
);
}
requestMock.mockImplementationOnce(() => ({
data: {
id: '1',
name: 'title_updated',
description: {
format: 'html',
content: 'desc_updated',
requestMock.mockImplementationOnce(() =>
createAxiosResponse({
data: {
id: '1',
name: 'title_updated',
description: {
format: 'html',
content: 'desc_updated',
},
inc_last_modified_date: 1589391874472,
},
inc_last_modified_date: 1589391874472,
},
}));
})
);
};
describe('IBM Resilient service', () => {
@ -207,24 +213,28 @@ describe('IBM Resilient service', () => {
describe('getIncident', () => {
test('it returns the incident correctly', async () => {
requestMock.mockImplementation(() => ({
data: {
id: '1',
name: '1',
description: {
format: 'html',
content: 'description',
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
id: '1',
name: '1',
description: {
format: 'html',
content: 'description',
},
},
},
}));
})
);
const res = await service.getIncident('1');
expect(res).toEqual({ id: '1', name: '1', description: 'description' });
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: { id: '1' },
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: { id: '1' },
})
);
await service.getIncident('1');
expect(requestMock).toHaveBeenCalledWith({
@ -246,28 +256,42 @@ describe('IBM Resilient service', () => {
'Unable to get incident with id 1. Error: An error has occurred'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.getIncident('1')).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
});
describe('createIncident', () => {
test('it creates the incident correctly', async () => {
requestMock.mockImplementation(() => ({
data: {
id: '1',
name: 'title',
description: 'description',
discovered_date: 1589391874472,
create_date: 1589391874472,
},
}));
const incident = {
incident: {
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 6,
},
};
const res = await service.createIncident({
incident: {
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 6,
},
});
test('it creates the incident correctly', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
id: '1',
name: 'title',
description: 'description',
discovered_date: 1589391874472,
create_date: 1589391874472,
},
})
);
const res = await service.createIncident(incident);
expect(res).toEqual({
title: '1',
@ -278,24 +302,19 @@ describe('IBM Resilient service', () => {
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: {
id: '1',
name: 'title',
description: 'description',
discovered_date: 1589391874472,
create_date: 1589391874472,
},
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
id: '1',
name: 'title',
description: 'description',
discovered_date: 1589391874472,
create_date: 1589391874472,
},
})
);
await service.createIncident({
incident: {
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 6,
},
});
await service.createIncident(incident);
expect(requestMock).toHaveBeenCalledWith({
axios,
@ -334,20 +353,39 @@ describe('IBM Resilient service', () => {
'[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.createIncident(incident)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
test('it should throw if the required attributes are not there', async () => {
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
await expect(service.createIncident(incident)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to create incident. Error: Response is missing at least one of the expected fields: id,create_date.'
);
});
});
describe('updateIncident', () => {
const req = {
incidentId: '1',
incident: {
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 6,
},
};
test('it updates the incident correctly', async () => {
mockIncidentUpdate();
const res = await service.updateIncident({
incidentId: '1',
incident: {
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 6,
},
});
const res = await service.updateIncident(req);
expect(res).toEqual({
title: '1',
@ -430,38 +468,59 @@ describe('IBM Resilient service', () => {
test('it should throw an error', async () => {
mockIncidentUpdate(true);
await expect(
service.updateIncident({
incidentId: '1',
incident: {
await expect(service.updateIncident(req)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
);
});
test('it should throw if the request is not a JSON', async () => {
// get incident request
requestMock.mockImplementationOnce(() =>
createAxiosResponse({
data: {
id: '1',
name: 'title',
description: 'desc',
incidentTypes: [1001],
severityCode: 5,
description: {
format: 'html',
content: 'description',
},
incident_type_ids: [1001, 16, 12],
severity_code: 6,
},
})
).rejects.toThrow(
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
);
// update incident request
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.updateIncident(req)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json'
);
});
});
describe('createComment', () => {
test('it creates the comment correctly', async () => {
requestMock.mockImplementation(() => ({
data: {
id: '1',
create_date: 1589391874472,
},
}));
const req = {
incidentId: '1',
comment: {
comment: 'comment',
commentId: 'comment-1',
},
};
const res = await service.createComment({
incidentId: '1',
comment: {
comment: 'comment',
commentId: 'comment-1',
},
});
test('it creates the comment correctly', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
id: '1',
create_date: 1589391874472,
},
})
);
const res = await service.createComment(req);
expect(res).toEqual({
commentId: 'comment-1',
@ -471,20 +530,16 @@ describe('IBM Resilient service', () => {
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: {
id: '1',
create_date: 1589391874472,
},
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
id: '1',
create_date: 1589391874472,
},
})
);
await service.createComment({
incidentId: '1',
comment: {
comment: 'comment',
commentId: 'comment-1',
},
});
await service.createComment(req);
expect(requestMock).toHaveBeenCalledWith({
axios,
@ -506,27 +561,31 @@ describe('IBM Resilient service', () => {
throw new Error('An error has occurred');
});
await expect(
service.createComment({
incidentId: '1',
comment: {
comment: 'comment',
commentId: 'comment-1',
},
})
).rejects.toThrow(
await expect(service.createComment(req)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.createComment(req)).rejects.toThrow(
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
});
describe('getIncidentTypes', () => {
test('it creates the incident correctly', async () => {
requestMock.mockImplementation(() => ({
data: {
values: incidentTypes,
},
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
values: incidentTypes,
},
})
);
const res = await service.getIncidentTypes();
@ -545,15 +604,27 @@ describe('IBM Resilient service', () => {
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.getIncidentTypes()).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get incident types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
});
describe('getSeverity', () => {
test('it creates the incident correctly', async () => {
requestMock.mockImplementation(() => ({
data: {
values: severity,
},
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: {
values: severity,
},
})
);
const res = await service.getSeverity();
@ -578,17 +649,29 @@ describe('IBM Resilient service', () => {
throw new Error('An error has occurred');
});
await expect(service.getIncidentTypes()).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
await expect(service.getSeverity()).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.getSeverity()).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get severity. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
});
describe('getFields', () => {
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: resilientFields,
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: resilientFields,
})
);
await service.getFields();
expect(requestMock).toHaveBeenCalledWith({
@ -598,10 +681,13 @@ describe('IBM Resilient service', () => {
url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields',
});
});
test('it returns common fields correctly', async () => {
requestMock.mockImplementation(() => ({
data: resilientFields,
}));
requestMock.mockImplementation(() =>
createAxiosResponse({
data: resilientFields,
})
);
const res = await service.getFields();
expect(res).toEqual(resilientFields);
});
@ -614,5 +700,15 @@ describe('IBM Resilient service', () => {
'Unable to get fields. Error: An error has occurred'
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
);
await expect(service.getFields()).rejects.toThrow(
'[Action][IBM Resilient]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
);
});
});
});

View file

@ -24,7 +24,7 @@ import {
} from './types';
import * as i18n from './translations';
import { getErrorMessage, request } from '../lib/axios_utils';
import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils';
import { ActionsConfigurationUtilities } from '../../actions_config';
const VIEW_INCIDENT_URL = `#incidents`;
@ -134,6 +134,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
return { ...res.data, description: res.data.description?.content ?? '' };
} catch (error) {
throw new Error(
@ -182,6 +186,11 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id', 'create_date'],
});
return {
title: `${res.data.id}`,
id: `${res.data.id}`,
@ -212,6 +221,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
if (!res.data.success) {
throw new Error(res.data.message);
}
@ -245,6 +258,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
return {
commentId: comment.commentId,
externalCommentId: res.data.id,
@ -270,6 +287,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const incidentTypes = res.data?.values ?? [];
return incidentTypes.map((type: { value: string; label: string }) => ({
id: type.value,
@ -292,6 +313,10 @@ export const createExternalService = (
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
const incidentTypes = res.data?.values ?? [];
return incidentTypes.map((type: { value: string; label: string }) => ({
id: type.value,
@ -312,6 +337,11 @@ export const createExternalService = (
logger,
configurationUtilities,
});
throwIfResponseIsNotValid({
res,
});
return res.data ?? [];
} catch (error) {
throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`));

View file

@ -10,7 +10,7 @@ import axios from 'axios';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { Logger } from '../../../../../../src/core/server';
import { actionsConfigMock } from '../../actions_config.mock';
import * as utils from '../lib/axios_utils';
import { request, createAxiosResponse } from '../lib/axios_utils';
import { createExternalService } from './service';
import { mappings } from './mocks';
import { ExternalService } from './types';
@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => {
});
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
const requestMock = request as jest.Mock;
const configurationUtilities = actionsConfigMock.create();
describe('Swimlane Service', () => {
@ -152,9 +152,7 @@ describe('Swimlane Service', () => {
};
test('it creates a record correctly', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
const res = await service.createRecord({
incident,
@ -169,9 +167,7 @@ describe('Swimlane Service', () => {
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
await service.createRecord({
incident,
@ -207,6 +203,24 @@ describe('Swimlane Service', () => {
`[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } })
);
await expect(service.createRecord({ incident })).rejects.toThrow(
`[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown`
);
});
test('it should throw if the required attributes are not there', async () => {
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
await expect(service.createRecord({ incident })).rejects.toThrow(
`[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,createdDate. Reason: unknown`
);
});
});
describe('updateRecord', () => {
@ -218,9 +232,7 @@ describe('Swimlane Service', () => {
const incidentId = '123';
test('it updates a record correctly', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
const res = await service.updateRecord({
incident,
@ -236,9 +248,7 @@ describe('Swimlane Service', () => {
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
await service.updateRecord({
incident,
@ -276,6 +286,24 @@ describe('Swimlane Service', () => {
`[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
);
});
test('it should throw if the request is not a JSON', async () => {
requestMock.mockImplementation(() =>
createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } })
);
await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow(
`[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown`
);
});
test('it should throw if the required attributes are not there', async () => {
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow(
`[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,modifiedDate. Reason: unknown`
);
});
});
describe('createComment', () => {
@ -289,9 +317,7 @@ describe('Swimlane Service', () => {
const createdDate = '2021-06-01T17:29:51.092Z';
test('it updates a record correctly', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
const res = await service.createComment({
comment,
@ -306,9 +332,7 @@ describe('Swimlane Service', () => {
});
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data,
}));
requestMock.mockImplementation(() => createAxiosResponse({ data }));
await service.createComment({
comment,

View file

@ -9,7 +9,7 @@ import { Logger } from '@kbn/logging';
import axios from 'axios';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { getErrorMessage, request } from '../lib/axios_utils';
import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils';
import { getBodyForEventAction } from './helpers';
import {
CreateCommentParams,
@ -89,6 +89,12 @@ export const createExternalService = (
method: 'post',
url: getPostRecordUrl(appId),
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id', 'name', 'createdDate'],
});
return {
id: res.data.id,
title: res.data.name,
@ -124,6 +130,11 @@ export const createExternalService = (
url: getPostRecordIdUrl(appId, params.incidentId),
});
throwIfResponseIsNotValid({
res,
requiredAttributesToBeInTheResponse: ['id', 'name', 'modifiedDate'],
});
return {
id: res.data.id,
title: res.data.name,

View file

@ -30,7 +30,6 @@ export function initPlugin(router: IRouter, path: string) {
return jsonResponse(res, 200, {
id: '123',
key: 'CK-1',
created: '2020-04-27T14:17:45.490Z',
});
}
);
@ -48,12 +47,7 @@ export function initPlugin(router: IRouter, path: string) {
req: KibanaRequest<any, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
return jsonResponse(res, 200, {
id: '123',
key: 'CK-1',
created: '2020-04-27T14:17:45.490Z',
updated: '2020-04-27T14:17:45.490Z',
});
return jsonResponse(res, 204, {});
}
);
@ -73,10 +67,12 @@ export function initPlugin(router: IRouter, path: string) {
return jsonResponse(res, 200, {
id: '123',
key: 'CK-1',
created: '2020-04-27T14:17:45.490Z',
updated: '2020-04-27T14:17:45.490Z',
summary: 'title',
description: 'description',
fields: {
created: '2020-04-27T14:17:45.490Z',
updated: '2020-04-27T14:17:45.490Z',
summary: 'title',
description: 'description',
},
});
}
);
@ -97,6 +93,7 @@ export function initPlugin(router: IRouter, path: string) {
return jsonResponse(res, 200, {
id: '123',
created: '2020-04-27T14:17:45.490Z',
updated: '2020-04-27T14:17:45.490Z',
});
}
);