kibana/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
Mike Côté da8ce374cf
Make xpack.actions.rejectUnauthorized setting work (#88690)
* Remove ActionsConfigType due to being a duplicate

* Fix rejectUnauthorized not being configured

* Move proxySettings to configurationUtilities

* Fix isAxiosError check to code

* Add functional test

* Remove comment

* Close webhook server

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2021-01-28 13:44:25 -05:00

499 lines
14 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/send_email', () => ({
sendEmail: jest.fn(),
}));
import { Logger } from '../../../../../src/core/server';
import { actionsConfigMock } from '../actions_config.mock';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { createActionTypeRegistry } from './index.test';
import { sendEmail } from './lib/send_email';
import { actionsMock } from '../mocks';
import {
ActionParamsType,
ActionTypeConfigType,
ActionTypeSecretsType,
getActionType,
EmailActionType,
EmailActionTypeExecutorOptions,
} from './email';
const sendEmailMock = sendEmail as jest.Mock;
const ACTION_TYPE_ID = '.email';
const services = actionsMock.createServices();
let actionType: EmailActionType;
let mockedLogger: jest.Mocked<Logger>;
beforeEach(() => {
jest.resetAllMocks();
const { actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType
>(ACTION_TYPE_ID);
});
describe('actionTypeRegistry.get() works', () => {
test('action type static data is as expected', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
expect(actionType.name).toEqual('Email');
});
});
describe('config validation', () => {
test('config validation succeeds when config is valid', () => {
const config: Record<string, unknown> = {
service: 'gmail',
from: 'bob@example.com',
hasAuth: true,
};
expect(validateConfig(actionType, config)).toEqual({
...config,
host: null,
port: null,
secure: null,
});
delete config.service;
config.host = 'elastic.co';
config.port = 8080;
config.hasAuth = true;
expect(validateConfig(actionType, config)).toEqual({
...config,
service: null,
secure: null,
});
});
test('config validation fails when config is not valid', () => {
const baseConfig: Record<string, unknown> = {
from: 'bob@example.com',
};
// empty object
expect(() => {
validateConfig(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [from]: expected value of type [string] but got [undefined]"`
);
// no service or host/port
expect(() => {
validateConfig(actionType, baseConfig);
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: either [service] or [host]/[port] is required"`
);
// host but no port
expect(() => {
validateConfig(actionType, { ...baseConfig, host: 'elastic.co' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [port] is required if [service] is not provided"`
);
// port but no host
expect(() => {
validateConfig(actionType, { ...baseConfig, port: 8080 });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [host] is required if [service] is not provided"`
);
// invalid service
expect(() => {
validateConfig(actionType, {
...baseConfig,
service: 'bad-nodemailer-service',
});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [service] value 'bad-nodemailer-service' is not valid"`
);
});
// nodemailer supports a service named 'AOL' that maps to the host below
const NODEMAILER_AOL_SERVICE = 'AOL';
const NODEMAILER_AOL_SERVICE_HOST = 'smtp.aol.com';
test('config validation handles email host in allowedHosts', () => {
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: {
...actionsConfigMock.create(),
isHostnameAllowed: (hostname) => hostname === NODEMAILER_AOL_SERVICE_HOST,
},
});
const baseConfig = {
from: 'bob@example.com',
};
const allowedHosts1 = {
...baseConfig,
service: NODEMAILER_AOL_SERVICE,
};
const allowedHosts2 = {
...baseConfig,
host: NODEMAILER_AOL_SERVICE_HOST,
port: 42,
};
const notAllowedHosts1 = {
...baseConfig,
service: 'gmail',
};
const notAllowedHosts2 = {
...baseConfig,
host: 'smtp.gmail.com',
port: 42,
};
const validatedConfig1 = validateConfig(actionType, allowedHosts1);
expect(validatedConfig1.service).toEqual(allowedHosts1.service);
expect(validatedConfig1.from).toEqual(allowedHosts1.from);
const validatedConfig2 = validateConfig(actionType, allowedHosts2);
expect(validatedConfig2.host).toEqual(allowedHosts2.host);
expect(validatedConfig2.port).toEqual(allowedHosts2.port);
expect(validatedConfig2.from).toEqual(allowedHosts2.from);
expect(() => {
validateConfig(actionType, notAllowedHosts1);
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the allowedHosts configuration"`
);
expect(() => {
validateConfig(actionType, notAllowedHosts2);
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [host] value 'smtp.gmail.com' is not in the allowedHosts configuration"`
);
});
});
describe('secrets validation', () => {
test('secrets validation succeeds when secrets is valid', () => {
const secrets: Record<string, unknown> = {
user: 'bob',
password: 'supersecret',
};
expect(validateSecrets(actionType, secrets)).toEqual(secrets);
});
test('secrets validation succeeds when secrets props are null/undefined', () => {
const secrets: Record<string, unknown> = {
user: null,
password: null,
};
expect(validateSecrets(actionType, {})).toEqual(secrets);
expect(validateSecrets(actionType, { user: null })).toEqual(secrets);
expect(validateSecrets(actionType, { password: null })).toEqual(secrets);
});
});
describe('params validation', () => {
test('params validation succeeds when params is valid', () => {
const params: Record<string, unknown> = {
to: ['bob@example.com'],
subject: 'this is a test',
message: 'this is the message',
};
expect(validateParams(actionType, params)).toMatchInlineSnapshot(`
Object {
"bcc": Array [],
"cc": Array [],
"kibanaFooterLink": Object {
"path": "/",
"text": "Go to Kibana",
},
"message": "this is the message",
"subject": "this is a test",
"to": Array [
"bob@example.com",
],
}
`);
});
test('params validation fails when params is not valid', () => {
// empty object
expect(() => {
validateParams(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action params: [subject]: expected value of type [string] but got [undefined]"`
);
});
});
describe('execute()', () => {
const config: ActionTypeConfigType = {
service: '__json',
host: 'a host',
port: 42,
secure: true,
from: 'bob@example.com',
hasAuth: true,
};
const secrets: ActionTypeSecretsType = {
user: 'bob',
password: 'supersecret',
};
const params: ActionParamsType = {
to: ['jim@example.com'],
cc: ['james@example.com'],
bcc: ['jimmy@example.com'],
subject: 'the subject',
message: 'a message to you',
kibanaFooterLink: {
path: '/',
text: 'Go to Kibana',
},
};
const actionId = 'some-id';
const executorOptions: EmailActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
test('ensure parameters are as expected', async () => {
sendEmailMock.mockReset();
const result = await actionType.executor(executorOptions);
expect(result).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"data": undefined,
"status": "ok",
}
`);
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
Object {
"configurationUtilities": Object {
"ensureActionTypeEnabled": [MockFunction],
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
"isUriAllowed": [MockFunction],
},
"content": Object {
"message": "a message to you
--
This message was sent by Kibana.",
"subject": "the subject",
},
"hasAuth": true,
"routing": Object {
"bcc": Array [
"jimmy@example.com",
],
"cc": Array [
"james@example.com",
],
"from": "bob@example.com",
"to": Array [
"jim@example.com",
],
},
"transport": Object {
"password": "supersecret",
"service": "__json",
"user": "bob",
},
}
`);
});
test('parameters are as expected with no auth', async () => {
const customExecutorOptions: EmailActionTypeExecutorOptions = {
...executorOptions,
config: {
...config,
service: null,
hasAuth: false,
},
secrets: {
...secrets,
user: null,
password: null,
},
};
sendEmailMock.mockReset();
await actionType.executor(customExecutorOptions);
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
Object {
"configurationUtilities": Object {
"ensureActionTypeEnabled": [MockFunction],
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
"isUriAllowed": [MockFunction],
},
"content": Object {
"message": "a message to you
--
This message was sent by Kibana.",
"subject": "the subject",
},
"hasAuth": false,
"routing": Object {
"bcc": Array [
"jimmy@example.com",
],
"cc": Array [
"james@example.com",
],
"from": "bob@example.com",
"to": Array [
"jim@example.com",
],
},
"transport": Object {
"host": "a host",
"port": 42,
"secure": true,
},
}
`);
});
test('returns expected result when an error is thrown', async () => {
const customExecutorOptions: EmailActionTypeExecutorOptions = {
...executorOptions,
config: {
...config,
service: null,
hasAuth: false,
},
secrets: {
...secrets,
user: null,
password: null,
},
};
sendEmailMock.mockReset();
sendEmailMock.mockRejectedValue(new Error('wops'));
const result = await actionType.executor(customExecutorOptions);
expect(result).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"message": "error sending email",
"serviceMessage": "wops",
"status": "error",
}
`);
});
test('renders parameter templates as expected', async () => {
expect(actionType.renderParameterTemplates).toBeTruthy();
const paramsWithTemplates = {
to: [],
cc: ['{{rogue}}'],
bcc: ['jim', '{{rogue}}', 'bob'],
subject: '{{rogue}}',
message: '{{rogue}}',
kibanaFooterLink: {
path: '/',
text: 'Go to Kibana',
},
};
const variables = {
rogue: '*bold*',
};
const renderedParams = actionType.renderParameterTemplates!(paramsWithTemplates, variables);
// Yes, this is tested in the snapshot below, but it's double-escaped there,
// so easier to see here that the escaping is correct.
expect(renderedParams.message).toBe('\\*bold\\*');
expect(renderedParams).toMatchInlineSnapshot(`
Object {
"bcc": Array [
"jim",
"*bold*",
"bob",
],
"cc": Array [
"*bold*",
],
"kibanaFooterLink": Object {
"path": "/",
"text": "Go to Kibana",
},
"message": "\\\\*bold\\\\*",
"subject": "*bold*",
"to": Array [],
}
`);
});
test('provides a footer link to Kibana when publicBaseUrl is defined', async () => {
const actionTypeWithPublicUrl = getActionType({
logger: mockedLogger,
configurationUtilities: actionsConfigMock.create(),
publicBaseUrl: 'https://localhost:1234/foo/bar',
});
await actionTypeWithPublicUrl.executor(executorOptions);
expect(sendEmailMock).toHaveBeenCalledTimes(1);
const sendMailCall = sendEmailMock.mock.calls[0][1];
expect(sendMailCall.content.message).toMatchInlineSnapshot(`
"a message to you
--
This message was sent by Kibana. [Go to Kibana](https://localhost:1234/foo/bar)."
`);
});
test('allows to generate a deep link into Kibana when publicBaseUrl is defined', async () => {
const actionTypeWithPublicUrl = getActionType({
logger: mockedLogger,
configurationUtilities: actionsConfigMock.create(),
publicBaseUrl: 'https://localhost:1234/foo/bar',
});
const customExecutorOptions: EmailActionTypeExecutorOptions = {
...executorOptions,
params: {
...params,
kibanaFooterLink: {
path: '/my/app',
text: 'View this in Kibana',
},
},
};
await actionTypeWithPublicUrl.executor(customExecutorOptions);
expect(sendEmailMock).toHaveBeenCalledTimes(1);
const sendMailCall = sendEmailMock.mock.calls[0][1];
expect(sendMailCall.content.message).toMatchInlineSnapshot(`
"a message to you
--
This message was sent by Kibana. [View this in Kibana](https://localhost:1234/foo/bar/my/app)."
`);
});
});