kibana/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts
Gidi Meir Morris 8547b32bab
[Actions] avoids setting a default dedupKey on PagerDuty (#77773)
The PagerDuty Action currently defaults to a dedupKey that's shared between all action executions of the same connector.
To ensure we don't group unrelated executions together this PR avoids setting a default, which means each execution will result in its own incident in PD.

As part of this change we've also made the `dedupKey` a required field whenever a `resolve` or `acknowledge` event_action is chosen. This ensure we don't try to resolve without a dedupKey, which would result in an error in PD.

A migration has been introduced to migrate existing alerts which might not have a `dedupKey` configured.
2020-09-28 14:56:20 +01:00

579 lines
17 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('./lib/post_pagerduty', () => ({
postPagerduty: jest.fn(),
}));
import { Services } from '../types';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { postPagerduty } from './lib/post_pagerduty';
import { createActionTypeRegistry } from './index.test';
import { Logger } from '../../../../../src/core/server';
import { actionsConfigMock } from '../actions_config.mock';
import { actionsMock } from '../mocks';
import {
ActionParamsType,
ActionTypeConfigType,
ActionTypeSecretsType,
getActionType,
PagerDutyActionType,
PagerDutyActionTypeExecutorOptions,
} from './pagerduty';
const postPagerdutyMock = postPagerduty as jest.Mock;
const ACTION_TYPE_ID = '.pagerduty';
const services: Services = actionsMock.createServices();
let actionType: PagerDutyActionType;
let mockedLogger: jest.Mocked<Logger>;
beforeAll(() => {
const { logger, actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType
>(ACTION_TYPE_ID);
mockedLogger = logger;
});
describe('get()', () => {
test('should return correct action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
expect(actionType.name).toEqual('PagerDuty');
});
});
describe('validateConfig()', () => {
test('should validate and pass when config is valid', () => {
expect(validateConfig(actionType, {})).toEqual({ apiUrl: null });
expect(validateConfig(actionType, { apiUrl: 'bar' })).toEqual({ apiUrl: 'bar' });
});
test('should validate and throw error when config is invalid', () => {
expect(() => {
validateConfig(actionType, { shouldNotBeHere: true });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [shouldNotBeHere]: definition for this key is missing"`
);
});
test('should validate and pass when the pagerduty url is added to allowedHosts', () => {
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: {
...actionsConfigMock.create(),
ensureUriAllowed: (url) => {
expect(url).toEqual('https://events.pagerduty.com/v2/enqueue');
},
},
});
expect(
validateConfig(actionType, { apiUrl: 'https://events.pagerduty.com/v2/enqueue' })
).toEqual({ apiUrl: 'https://events.pagerduty.com/v2/enqueue' });
});
test('config validation returns an error if the specified URL isnt added to allowedHosts', () => {
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: {
...actionsConfigMock.create(),
ensureUriAllowed: (_) => {
throw new Error(`target url is not added to allowedHosts`);
},
},
});
expect(() => {
validateConfig(actionType, { apiUrl: 'https://events.pagerduty.com/v2/enqueue' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: error configuring pagerduty action: target url is not added to allowedHosts"`
);
});
});
describe('validateSecrets()', () => {
test('should validate and pass when secrets is valid', () => {
const routingKey = 'super-secret';
expect(validateSecrets(actionType, { routingKey })).toEqual({
routingKey,
});
});
test('should validate and throw error when secrets is invalid', () => {
expect(() => {
validateSecrets(actionType, { routingKey: false });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [routingKey]: expected value of type [string] but got [boolean]"`
);
expect(() => {
validateSecrets(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]"`
);
});
});
describe('validateParams()', () => {
test('should validate and pass when params is valid', () => {
expect(validateParams(actionType, {})).toEqual({});
const params = {
eventAction: 'trigger',
dedupKey: 'a dedupKey',
summary: 'a summary',
source: 'a source',
severity: 'critical',
timestamp: new Date().toISOString(),
component: 'a component',
group: 'a group',
class: 'a class',
};
expect(validateParams(actionType, params)).toEqual(params);
});
test('should validate and throw error when params is invalid', () => {
expect(() => {
validateParams(actionType, { eventAction: 'ackynollage' });
}).toThrowErrorMatchingInlineSnapshot(`
"error validating action params: [eventAction]: types that failed validation:
- [eventAction.0]: expected value to equal [trigger]
- [eventAction.1]: expected value to equal [resolve]
- [eventAction.2]: expected value to equal [acknowledge]"
`);
});
test('should validate and throw error when timestamp has spaces', () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const timestamp = ` ${randoDate}`;
expect(() => {
validateParams(actionType, {
timestamp,
});
}).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`);
});
test('should validate and throw error when timestamp is invalid', () => {
const timestamp = `1963-09-55 90:23:45`;
expect(() => {
validateParams(actionType, {
timestamp,
});
}).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`);
});
test('should validate and throw error when dedupKey is missing on resolve', () => {
expect(() => {
validateParams(actionType, {
eventAction: 'resolve',
});
}).toThrowError(
`error validating action params: DedupKey is required when eventAction is "resolve"`
);
});
});
describe('execute()', () => {
beforeEach(() => {
postPagerdutyMock.mockReset();
});
test('should succeed with minimal valid params', async () => {
const secrets = { routingKey: 'super-secret' };
const config = { apiUrl: null };
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "https://events.pagerduty.com/v2/enqueue",
"data": Object {
"event_action": "trigger",
"payload": Object {
"severity": "info",
"source": "Kibana Action some-action-id",
"summary": "No summary provided.",
},
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for trigger', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params: ActionParamsType = {
eventAction: 'trigger',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "trigger",
"payload": Object {
"class": "the-class",
"component": "the-component",
"group": "the-group",
"severity": "critical",
"source": "the-source",
"summary": "the summary",
"timestamp": "1963-09-23T01:23:45.000Z",
},
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for acknowledge', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params: ActionParamsType = {
eventAction: 'acknowledge',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "acknowledge",
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});
test('should succeed with maximal valid params for resolve', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params: ActionParamsType = {
eventAction: 'resolve',
dedupKey: 'a-dedup-key',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
component: 'the-component',
group: 'the-group',
class: 'the-class',
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"dedup_key": "a-dedup-key",
"event_action": "resolve",
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});
test('should fail when sendPagerdury throws', async () => {
const secrets = { routingKey: 'super-secret' };
const config = { apiUrl: null };
const params = {};
postPagerdutyMock.mockImplementation(() => {
throw new Error('doing some testing');
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting pagerduty event",
"serviceMessage": "doing some testing",
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 429', async () => {
const secrets = { routingKey: 'super-secret' };
const config = { apiUrl: null };
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 429, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting pagerduty event: http status 429, retry later",
"retry": true,
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 501', async () => {
const secrets = { routingKey: 'super-secret' };
const config = { apiUrl: null };
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 501, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting pagerduty event: http status 501, retry later",
"retry": true,
"status": "error",
}
`);
});
test('should fail when sendPagerdury returns 418', async () => {
const secrets = { routingKey: 'super-secret' };
const config = { apiUrl: null };
const params = {};
postPagerdutyMock.mockImplementation(() => {
return { status: 418, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting pagerduty event: unexpected status 418",
"status": "error",
}
`);
});
test('should not set a default dedupkey to ensure each execution is a unique PagerDuty incident', async () => {
const randoDate = new Date('1963-09-23T01:23:45Z').toISOString();
const secrets = {
routingKey: 'super-secret',
};
const config = {
apiUrl: 'the-api-url',
};
const params: ActionParamsType = {
eventAction: 'trigger',
summary: 'the summary',
source: 'the-source',
severity: 'critical',
timestamp: randoDate,
};
postPagerdutyMock.mockImplementation(() => {
return { status: 202, data: 'data-here' };
});
const actionId = 'some-action-id';
const executorOptions: PagerDutyActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
expect({ apiUrl, data, headers }).toMatchInlineSnapshot(`
Object {
"apiUrl": "the-api-url",
"data": Object {
"event_action": "trigger",
"payload": Object {
"severity": "critical",
"source": "the-source",
"summary": "the summary",
"timestamp": "1963-09-23T01:23:45.000Z",
},
},
"headers": Object {
"Content-Type": "application/json",
"X-Routing-Key": "super-secret",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});
});