Remove notifications plugin (#41674)
The notifications functionality has been replaced by the features of the actions plugin. This notifications plugin was never actually used by end-user facing features of Kibana.
This commit is contained in:
parent
a177b86258
commit
6419c232e5
|
@ -26,7 +26,6 @@ import { indexManagement } from './legacy/plugins/index_management';
|
|||
import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management';
|
||||
import { consoleExtensions } from './legacy/plugins/console_extensions';
|
||||
import { spaces } from './legacy/plugins/spaces';
|
||||
import { notifications } from './legacy/plugins/notifications';
|
||||
import { kueryAutocomplete } from './legacy/plugins/kuery_autocomplete';
|
||||
import { canvas } from './legacy/plugins/canvas';
|
||||
import { infra } from './legacy/plugins/infra';
|
||||
|
@ -70,7 +69,6 @@ module.exports = function (kibana) {
|
|||
cloud(kibana),
|
||||
indexManagement(kibana),
|
||||
consoleExtensions(kibana),
|
||||
notifications(kibana),
|
||||
indexLifecycleManagement(kibana),
|
||||
kueryAutocomplete(kibana),
|
||||
infra(kibana),
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* User-configurable settings for xpack.notifications via configuration schema
|
||||
*
|
||||
* @param {Object} Joi - HapiJS Joi module that allows for schema validation
|
||||
* @return {Object} config schema
|
||||
*/
|
||||
export const config = (Joi) => {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
email: Joi.object({
|
||||
enabled: Joi.boolean().default(false),
|
||||
smtp: Joi.object({
|
||||
host: Joi.string().default('localhost'),
|
||||
port: Joi.number().default(25),
|
||||
require_tls: Joi.boolean().default(false),
|
||||
pool: Joi.boolean().default(false),
|
||||
auth: Joi.object({
|
||||
username: Joi.string(),
|
||||
password: Joi.string()
|
||||
}).default(),
|
||||
}).default(),
|
||||
defaults: Joi.object({
|
||||
from: Joi.string(),
|
||||
to: Joi.array().single().items(Joi.string()),
|
||||
cc: Joi.array().single().items(Joi.string()),
|
||||
bcc: Joi.array().single().items(Joi.string()),
|
||||
}).default(),
|
||||
}).default(),
|
||||
slack: Joi.object({
|
||||
enabled: Joi.boolean().default(false),
|
||||
token: Joi.string().required(),
|
||||
defaults: Joi.object({
|
||||
channel: Joi.string(),
|
||||
as_user: Joi.boolean().default(false),
|
||||
icon_emoji: Joi.string(),
|
||||
icon_url: Joi.string(),
|
||||
link_names: Joi.boolean().default(true),
|
||||
mrkdwn: Joi.boolean().default(true),
|
||||
unfurl_links: Joi.boolean().default(true),
|
||||
unfurl_media: Joi.boolean().default(true),
|
||||
username: Joi.string(),
|
||||
}).default(),
|
||||
})
|
||||
}).default();
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { init } from './init';
|
||||
import { config } from './config';
|
||||
|
||||
/**
|
||||
* Invokes plugin modules to instantiate the Notification plugin for Kibana
|
||||
*
|
||||
* @param kibana {Object} Kibana plugin instance
|
||||
* @return {Object} Notification Kibana plugin object
|
||||
*/
|
||||
export const notifications = (kibana) => new kibana.Plugin({
|
||||
require: ['kibana', 'xpack_main'],
|
||||
id: 'notifications',
|
||||
configPrefix: 'xpack.notifications',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
init,
|
||||
config,
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import {
|
||||
notificationService,
|
||||
createEmailAction,
|
||||
createSlackAction,
|
||||
LoggerAction,
|
||||
} from './server';
|
||||
import { notificationServiceSendRoute } from './server/routes/api/v1/notifications';
|
||||
|
||||
/**
|
||||
* Initialize the Action Service with various actions provided by X-Pack, when configured.
|
||||
*
|
||||
* @param server {Object} HapiJS server instance
|
||||
*/
|
||||
export function init(server) {
|
||||
const config = server.config();
|
||||
|
||||
// the logger
|
||||
notificationService.setAction(new LoggerAction({ server }));
|
||||
|
||||
if (config.get('xpack.notifications.email.enabled')) {
|
||||
notificationService.setAction(createEmailAction(server));
|
||||
}
|
||||
|
||||
if (config.get('xpack.notifications.slack.enabled')) {
|
||||
notificationService.setAction(createSlackAction(server));
|
||||
}
|
||||
|
||||
notificationServiceSendRoute(server, notificationService);
|
||||
|
||||
// expose the notification service for other plugins
|
||||
server.expose('notificationService', notificationService);
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { EmailAction } from './email_action';
|
||||
|
||||
/**
|
||||
* Create a Nodemailer transporter options object from the config.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Nodemailer.
|
||||
*/
|
||||
export function optionsFromConfig(config) {
|
||||
return {
|
||||
host: config.get('xpack.notifications.email.smtp.host'),
|
||||
port: config.get('xpack.notifications.email.smtp.port'),
|
||||
requireTLS: config.get('xpack.notifications.email.smtp.require_tls'),
|
||||
pool: config.get('xpack.notifications.email.smtp.pool'),
|
||||
auth: {
|
||||
user: config.get('xpack.notifications.email.smtp.auth.username'),
|
||||
pass: config.get('xpack.notifications.email.smtp.auth.password'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Nodemailer defaults object from the config.
|
||||
*
|
||||
* Defaults include things like the default "from" email address.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Nodemailer on a per-message basis.
|
||||
*/
|
||||
export function defaultsFromConfig(config) {
|
||||
return {
|
||||
from: config.get('xpack.notifications.email.defaults.from'),
|
||||
to: config.get('xpack.notifications.email.defaults.to'),
|
||||
cc: config.get('xpack.notifications.email.defaults.cc'),
|
||||
bcc: config.get('xpack.notifications.email.defaults.bcc'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Email Action based on the configuration.
|
||||
*
|
||||
* @param {Object} server The server object.
|
||||
* @return {EmailAction} A new email action based on the kibana.yml configuration.
|
||||
*/
|
||||
export function createEmailAction(server, { _options = optionsFromConfig, _defaults = defaultsFromConfig } = { }) {
|
||||
const config = server.config();
|
||||
|
||||
const options = _options(config);
|
||||
const defaults = _defaults(config);
|
||||
|
||||
return new EmailAction({ server, options, defaults });
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { EmailAction } from './email_action';
|
||||
import {
|
||||
createEmailAction,
|
||||
defaultsFromConfig,
|
||||
optionsFromConfig,
|
||||
} from './create_email_action';
|
||||
|
||||
describe('create_email_action', () => {
|
||||
|
||||
test('optionsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'host',
|
||||
'port',
|
||||
'require_tls',
|
||||
'pool',
|
||||
'auth.username',
|
||||
'auth.password',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.email.smtp.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(optionsFromConfig({ get })).toEqual({
|
||||
host: 'host',
|
||||
port: 'port',
|
||||
requireTLS: 'require_tls',
|
||||
pool: 'pool',
|
||||
auth: {
|
||||
user: 'auth.username',
|
||||
pass: 'auth.password',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('defaultsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'from',
|
||||
'to',
|
||||
'cc',
|
||||
'bcc',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.email.defaults.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(defaultsFromConfig({ get })).toEqual({
|
||||
from: 'from',
|
||||
to: 'to',
|
||||
cc: 'cc',
|
||||
bcc: 'bcc',
|
||||
});
|
||||
});
|
||||
|
||||
test('createEmailAction', async () => {
|
||||
const config = { };
|
||||
const server = { config: jest.fn().mockReturnValue(config) };
|
||||
const _options = jest.fn().mockReturnValue({ options: true });
|
||||
const defaults = { defaults: true };
|
||||
const _defaults = jest.fn().mockReturnValue(defaults);
|
||||
|
||||
const action = createEmailAction(server, { _options, _defaults });
|
||||
|
||||
expect(action instanceof EmailAction).toBe(true);
|
||||
expect(action.defaults).toBe(defaults);
|
||||
|
||||
expect(server.config).toHaveBeenCalledTimes(1);
|
||||
expect(server.config).toHaveBeenCalledWith();
|
||||
expect(_options).toHaveBeenCalledTimes(1);
|
||||
expect(_options).toHaveBeenCalledWith(config);
|
||||
expect(_defaults).toHaveBeenCalledTimes(1);
|
||||
expect(_defaults).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const EMAIL_ACTION_ID = 'xpack-notifications-email';
|
||||
|
||||
/**
|
||||
* Email Action enables generic sending of emails, when configured.
|
||||
*/
|
||||
export class EmailAction extends Action {
|
||||
|
||||
/**
|
||||
* Create a new Action capable of sending emails.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {Object} options Configuration options for Nodemailer.
|
||||
* @param {Object} defaults Default fields used when sending emails.
|
||||
* @param {Object} _nodemailer Exposed for tests.
|
||||
*/
|
||||
constructor({ server, options, defaults = { }, _nodemailer = nodemailer }) {
|
||||
super({ server, id: EMAIL_ACTION_ID, name: 'Email' });
|
||||
|
||||
this.transporter = _nodemailer.createTransport(options, defaults);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(notification) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.to) && !Boolean(notification.to)) {
|
||||
missingFields.push({
|
||||
field: 'to',
|
||||
name: 'To',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(this.defaults.from) && !Boolean(notification.from)) {
|
||||
missingFields.push({
|
||||
field: 'from',
|
||||
name: 'From',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(notification.subject)) {
|
||||
missingFields.push({
|
||||
field: 'subject',
|
||||
name: 'Subject',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(notification.markdown)) {
|
||||
missingFields.push({
|
||||
field: 'markdown',
|
||||
name: 'Body',
|
||||
type: 'markdown',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
// this responds with a boolean 'true' response, otherwise throws an Error
|
||||
const response = await this.transporter.verify();
|
||||
|
||||
return new ActionResult({
|
||||
message: `Email action SMTP configuration has been verified.`,
|
||||
response: {
|
||||
verified: response
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
// Note: This throws an Error upon failure
|
||||
const response = await this.transporter.sendMail({
|
||||
// email routing
|
||||
from: notification.from,
|
||||
to: notification.to,
|
||||
cc: notification.cc,
|
||||
bcc: notification.bcc,
|
||||
// email content
|
||||
subject: notification.subject,
|
||||
html: notification.markdown,
|
||||
text: notification.markdown,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Sent email for '${notification.subject}'.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ActionResult } from '../';
|
||||
import { EMAIL_ACTION_ID, EmailAction } from './email_action';
|
||||
|
||||
describe('EmailAction', () => {
|
||||
|
||||
const server = { };
|
||||
const options = { options: true };
|
||||
const defaults = { defaults: true };
|
||||
const transporter = {
|
||||
// see beforeEach
|
||||
};
|
||||
const _nodemailer = {
|
||||
// see beforeEach
|
||||
};
|
||||
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
transporter.verify = jest.fn();
|
||||
transporter.sendMail = jest.fn();
|
||||
_nodemailer.createTransport = jest.fn().mockReturnValue(transporter);
|
||||
|
||||
action = new EmailAction({ server, options, defaults, _nodemailer });
|
||||
});
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.getId()).toBe(EMAIL_ACTION_ID);
|
||||
expect(action.getName()).toBe('Email');
|
||||
expect(action.transporter).toBe(transporter);
|
||||
|
||||
expect(_nodemailer.createTransport).toHaveBeenCalledTimes(1);
|
||||
expect(_nodemailer.createTransport).toHaveBeenCalledWith(options, defaults);
|
||||
});
|
||||
|
||||
describe('getMissingFields', () => {
|
||||
|
||||
test('returns missing fields', () => {
|
||||
const to = { field: 'to', name: 'To', type: 'email' };
|
||||
const from = { field: 'from', name: 'From', type: 'email' };
|
||||
const subject = { field: 'subject', name: 'Subject', type: 'text' };
|
||||
const markdown = { field: 'markdown', name: 'Body', type: 'markdown' };
|
||||
|
||||
const missing = [
|
||||
{ defaults: { }, notification: { }, missing: [ to, from, subject, markdown, ], },
|
||||
{ defaults: { }, notification: { from: 'b@c.co', subject: 'subject', markdown: 'body', }, missing: [ to, ], },
|
||||
{ defaults: { from: 'b@c.co', }, notification: { subject: 'subject', markdown: 'body', }, missing: [ to, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', subject: 'subject', markdown: 'body', }, missing: [ from, ], },
|
||||
{ defaults: { to: 'a@b.co', }, notification: { subject: 'subject', markdown: 'body', }, missing: [ from, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', markdown: 'body', }, missing: [ subject, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', subject: 'subject', }, missing: [ markdown, ], },
|
||||
];
|
||||
|
||||
missing.forEach(check => {
|
||||
const newDefaultsAction = new EmailAction({ server, defaults: check.defaults, _nodemailer });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual(check.missing);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns [] when all fields exist', () => {
|
||||
const exists = [
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { to: 'a@b.co', }, notification: { from: 'b@c.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { from: 'b@c.co', }, notification: { to: 'a@b.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { to: 'a@b.co', from: 'b@c.co', }, notification: { subject: 'subject', markdown: 'body', }, },
|
||||
];
|
||||
|
||||
exists.forEach(check => {
|
||||
const newDefaultsAction = new EmailAction({ server, defaults: check.defaults, _nodemailer });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformHealthCheck', () => {
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
transporter.verify.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(transporter.verify).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.verify).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
transporter.verify.mockResolvedValue(true);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Email action SMTP configuration has been verified.');
|
||||
expect(result.getResponse()).toEqual({
|
||||
verified: true
|
||||
});
|
||||
|
||||
expect(transporter.verify).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.verify).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformAction', () => {
|
||||
const email = { subject: 'email', markdown: 'body' };
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
transporter.sendMail.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformAction(email))
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(transporter.sendMail).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith({
|
||||
to: undefined,
|
||||
from: undefined,
|
||||
cc: undefined,
|
||||
bcc: undefined,
|
||||
subject: email.subject,
|
||||
html: email.markdown,
|
||||
text: email.markdown,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { fake: true };
|
||||
|
||||
transporter.sendMail.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(email);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Sent email for '${email.subject}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(transporter.sendMail).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith({
|
||||
to: undefined,
|
||||
from: undefined,
|
||||
cc: undefined,
|
||||
bcc: undefined,
|
||||
subject: email.subject,
|
||||
html: email.markdown,
|
||||
text: email.markdown,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createEmailAction } from './create_email_action';
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
notificationService,
|
||||
Action,
|
||||
ActionResult,
|
||||
} from './service';
|
||||
export { createEmailAction } from './email';
|
||||
export { createSlackAction } from './slack';
|
||||
export { LoggerAction } from './logger';
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { LoggerAction } from './logger_action';
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const LOGGER_ACTION_ID = 'xpack-notifications-logger';
|
||||
|
||||
/**
|
||||
* Logger Action enables generic logging of information into Kibana's logs.
|
||||
*
|
||||
* This is mostly useful for debugging.
|
||||
*/
|
||||
export class LoggerAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: LOGGER_ACTION_ID, name: 'Log' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return new ActionResult({
|
||||
message: `Logger action is always usable.`,
|
||||
response: { },
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
this.server.log([LOGGER_ACTION_ID, 'info'], notification);
|
||||
|
||||
return new ActionResult({
|
||||
message: 'Logged data returned as response.',
|
||||
response: notification
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ActionResult } from '../';
|
||||
import { LOGGER_ACTION_ID, LoggerAction } from './logger_action';
|
||||
|
||||
describe('LoggerAction', () => {
|
||||
|
||||
const action = new LoggerAction({ server: { } });
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.id).toBe(LOGGER_ACTION_ID);
|
||||
expect(action.name).toBe('Log');
|
||||
});
|
||||
|
||||
test('getMissingFields to return []', () => {
|
||||
expect(action.getMissingFields()).toEqual([]);
|
||||
});
|
||||
|
||||
test('doPerformHealthCheck returns ActionResult', async () => {
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Logger action is always usable.');
|
||||
expect(result.getResponse()).toEqual({ });
|
||||
});
|
||||
|
||||
test('doPerformAction logs and returns ActionResult', async () => {
|
||||
const notification = { fake: true };
|
||||
|
||||
const logger = jest.fn();
|
||||
const server = { log: logger };
|
||||
const action = new LoggerAction({ server });
|
||||
|
||||
const result = await action.doPerformAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Logged data returned as response.');
|
||||
expect(result.getResponse()).toBe(notification);
|
||||
|
||||
expect(logger).toHaveBeenCalledTimes(1);
|
||||
expect(logger).toHaveBeenCalledWith([LOGGER_ACTION_ID, 'info'], notification);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { notificationServiceSendRoute } from './notify';
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { boomify } from 'boom';
|
||||
|
||||
/**
|
||||
* Check the incoming request parameters to see if the action should be allowed to fire.
|
||||
*
|
||||
* @param {Object|null} action The action selected by the user.
|
||||
* @param {String} actionId The ID of the requested action from the user.
|
||||
* @param {Object} data The incoming data from the user.
|
||||
* @returns {Object|null} The error object, or null if no error.
|
||||
*/
|
||||
export function checkForErrors(action, actionId, data) {
|
||||
if (action === null) {
|
||||
return {
|
||||
message: `Unrecognized action: '${actionId}'.`,
|
||||
};
|
||||
} else {
|
||||
let validLicense = false;
|
||||
|
||||
try {
|
||||
validLicense = action.isLicenseValid();
|
||||
} catch (e) {
|
||||
// validLicense === false
|
||||
}
|
||||
|
||||
if (validLicense === false) {
|
||||
return {
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fields = action.getMissingFields(data);
|
||||
|
||||
if (fields.length !== 0) {
|
||||
return {
|
||||
message: `Unable to perform '${action.name}' action due to missing required fields.`,
|
||||
fields
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to send the {@code data} as a notification.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {NotificationService} notificationService The notification service singleton.
|
||||
* @param {String} actionId The specified action's ID.
|
||||
* @param {Function} data The notification data to send via the specified action.
|
||||
* @param {Function} _checkForErrors Exposed for testing.
|
||||
*/
|
||||
export async function sendNotification(server, notificationService, actionId, data, { _checkForErrors = checkForErrors } = { }) {
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const error = _checkForErrors(action, actionId, data);
|
||||
|
||||
if (error === null) {
|
||||
return action.performAction(data)
|
||||
.then(result => result.toJson())
|
||||
.catch(err => boomify(err)); // by API definition, this should never happen as performAction isn't allow to throw errrors
|
||||
}
|
||||
|
||||
server.log(['actions', 'error'], error.message);
|
||||
|
||||
return {
|
||||
status_code: 400,
|
||||
ok: false,
|
||||
message: `Error: ${error.message}`,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification Service route to perform actions (aka send data).
|
||||
*/
|
||||
export function notificationServiceSendRoute(server, notificationService) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/notifications/v1/notify',
|
||||
config: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
action: Joi.string().required(),
|
||||
data: Joi.object({
|
||||
from: Joi.string(),
|
||||
to: Joi.string(),
|
||||
subject: Joi.string().required(),
|
||||
markdown: Joi.string(),
|
||||
}).required()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: (req) => {
|
||||
const actionId = req.payload.action;
|
||||
const data = req.payload.data;
|
||||
|
||||
sendNotification(server, notificationService, actionId, data);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { checkForErrors, sendNotification } from './notify';
|
||||
import { boomify } from 'boom';
|
||||
|
||||
describe('notifications/routes/send', () => {
|
||||
|
||||
const id = 'notifications-test';
|
||||
const notification = { fake: true };
|
||||
|
||||
describe('checkForErrors', () => {
|
||||
|
||||
it('returns unrecognized action for null action', () => {
|
||||
expect(checkForErrors(null, id, { })).toEqual({
|
||||
message: `Unrecognized action: '${id}'.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid license if license check throws an error', () => {
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => {
|
||||
throw new Error();
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, { })).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid license if license is invalid', () => {
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => false,
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, { })).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fields related to missing data', () => {
|
||||
const fields = [ { field: 1 } ];
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => true,
|
||||
getMissingFields: (data) => {
|
||||
expect(data).toBe(notification);
|
||||
|
||||
return fields;
|
||||
},
|
||||
};
|
||||
|
||||
const error = checkForErrors(action, id, notification);
|
||||
|
||||
expect(error).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to missing required fields.`,
|
||||
fields
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if action is usable', () => {
|
||||
const notification = { fake: true };
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => true,
|
||||
getMissingFields: (data) => {
|
||||
expect(data).toBe(notification);
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, notification)).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('sendNotification', () => {
|
||||
|
||||
it('replies with error object for bad request', async () => {
|
||||
const error = {
|
||||
message: 'TEST - expected',
|
||||
fields: [ { fake: 1 } ],
|
||||
};
|
||||
const action = { };
|
||||
const server = {
|
||||
log: jest.fn(),
|
||||
};
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(error);
|
||||
|
||||
const sendResponse = await sendNotification(server, notificationService, id, notification, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
expect(server.log).toHaveBeenCalledTimes(1);
|
||||
expect(server.log).toHaveBeenCalledWith(['actions', 'error'], error.message);
|
||||
|
||||
expect(sendResponse).toEqual({
|
||||
status_code: 400,
|
||||
ok: false,
|
||||
message: `Error: ${error.message}`,
|
||||
error,
|
||||
});
|
||||
});
|
||||
|
||||
it('replies with action result JSON', async () => {
|
||||
const response = { ok: true, message: 'Test' };
|
||||
const result = {
|
||||
toJson: () => response,
|
||||
};
|
||||
const action = {
|
||||
performAction: jest.fn().mockReturnValue(Promise.resolve(result))
|
||||
};
|
||||
const server = { };
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(null);
|
||||
|
||||
const sendResponse = await sendNotification(server, notificationService, id, notification, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
|
||||
expect(sendResponse).toEqual(response);
|
||||
});
|
||||
|
||||
it('replies with unexpected result error', async () => {
|
||||
const error = new Error();
|
||||
const action = {
|
||||
performAction: jest.fn().mockReturnValue(Promise.reject(error))
|
||||
};
|
||||
const server = { };
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(null);
|
||||
|
||||
const sendResponse = await sendNotification(server, notificationService, id, notification, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
|
||||
expect(sendResponse).toEqual(boomify(error));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,575 +0,0 @@
|
|||
# Notification Service / Actions
|
||||
|
||||
Use this service to send notifications to users. An example of a notification is sending an email or
|
||||
explicitly adding something to the Kibana log.
|
||||
|
||||
Notifications are inherently asynchronous actions because of the likelihood that any notification is
|
||||
interacting with a remote server or service.
|
||||
|
||||
## Referencing the Notification Service
|
||||
|
||||
Note: Both of these may change in the future.
|
||||
|
||||
### Server Side
|
||||
|
||||
```js
|
||||
const notificationService = server.plugins.notifications.notificationService;
|
||||
|
||||
const action = notificationService.getActionForId('xpack-notifications-logger');
|
||||
const result = action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
### HTTP
|
||||
|
||||
```http
|
||||
POST /api/notifications/v1/notify
|
||||
{
|
||||
"action": "xpack-notifications-logger",
|
||||
"data": {
|
||||
"arbitrary": "payload",
|
||||
"can": "have multiple",
|
||||
"fields": [ 1, 2, 3 ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interfaces
|
||||
|
||||
There are two interfaces that are important from this package. `NotificationService`, which is exposed as
|
||||
a singleton from the plugin: `server.plugins.notifications.notificationService`. And `Action`, which
|
||||
provides an abstract JavaScript `class` to implement new `Action`s.
|
||||
|
||||
### NotificationService Interface
|
||||
|
||||
The `NotificationService` currently has four methods defined with very distinct purposes:
|
||||
|
||||
1. `setAction` is intended for plugin authors to add actions that do not exist with the basic notifications
|
||||
service.
|
||||
2. `removeAction` is intended for replacing existing plugins (e.g., augmenting another action).
|
||||
3. `getActionForId` enables explicitly fetching an action by its known ID.
|
||||
4. `getActionsForData` enables discovering compatible actions given an arbitrary set of data.
|
||||
|
||||
Note: Mutating the Notification Service should generally only be done based on very specific reasons,
|
||||
such as plugin initialization or the user dynamically configuring a service's availability (e.g.,
|
||||
if we support a secure data store, the user could theoretically provide authentication details for an email
|
||||
server).
|
||||
|
||||
It is also possible that the user will want to configure multiple variants of the same action, such as
|
||||
multiple email notifications with differing defaults. In that case, the action's ID may need to be
|
||||
reconsidered.
|
||||
|
||||
#### `setAction()`
|
||||
|
||||
This is the only way to add an `Action` instance. Instances are expected to be extensions of the `Action`
|
||||
class defined here. If the provided action already exists, then the old one is removed.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
notificationService.setAction(action);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `action` | Action | The unique action to support. |
|
||||
|
||||
###### Returns
|
||||
|
||||
Nothing.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple logging action that can be triggered generically.
|
||||
|
||||
```js
|
||||
class LoggerAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-logger', name: 'Log' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return new ActionResult({
|
||||
message: `Logger action is always usable.`,
|
||||
response: { },
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
this.server.log(['logger', 'info'], notification);
|
||||
|
||||
return new ActionResult({
|
||||
message: 'Logged data returned as response.',
|
||||
response: notification
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// It's possible that someone may choose to make the LoggerAction's log level configurable, so
|
||||
// replacing it could be done by re-setting it, which means any follow-on usage would use the new level
|
||||
// (or, possibly, you could create different actions entirely for different levels)
|
||||
notificationService.setAction(new LoggerAction({ server }));
|
||||
```
|
||||
|
||||
#### `removeAction()`
|
||||
|
||||
Remove an action that has been set.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const action = notificationService.removeAction(actionId);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | ID of the action to remove. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action \| null | The action that was removed. `null` otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
const action = notificationService.removeAction('xpack-notifications-logger');
|
||||
|
||||
if (action !== null) {
|
||||
// removed; otherwise it didn't exist (maybe it was already removed)
|
||||
}
|
||||
```
|
||||
|
||||
#### `getActionForId()`
|
||||
|
||||
Retrieve a specific `Action` from the Notification Service.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | ID of the action to retrieve. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action \| null | The action that was requested. `null` otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
// In this case, the ID is known from the earlier example
|
||||
const action = notificationService.getActionForId('xpack-notifications-logger');
|
||||
|
||||
if (action !== null) {
|
||||
// otherwise it didn't exist
|
||||
}
|
||||
```
|
||||
|
||||
#### `getActionsForData()`
|
||||
|
||||
Retrieve any `Action`s from the Notification Service that accept the supplied data, which is useful for
|
||||
discovery.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const actions = notificationService.getActionsForData(notification);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | Payload to send notification. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action[] | The actions that accept a subset of the data. Empty array otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
// In this case, the ID is known from the earlier example
|
||||
const actions = notificationService.getActionsForData({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (action.length !== 0) {
|
||||
// otherwise nothing matches
|
||||
}
|
||||
```
|
||||
|
||||
### Action Interface
|
||||
|
||||
From the perspective of developers that want to make use of `Action`s, there are three relevant methods:
|
||||
|
||||
1. `getMissingFields` provides an array of fields as well as the expected data type that did not exist in the
|
||||
supplied data.
|
||||
2. `performHealthCheck` attempts to perform a health check against the actions backing service.
|
||||
3. `performAction` attempts to perform the purpose of the action (e.g., send the email) using the supplied
|
||||
data.
|
||||
|
||||
For developers to create new `Action`s, there are three related methods:
|
||||
|
||||
1. `getMissingFields` provides an array of fields as well as the expected data type that did not exist in the
|
||||
supplied data.
|
||||
2. `doPerformHealthCheck` attempts to perform a health check against the actions backing service.
|
||||
- `performHealthCheck` invokes this method and wraps it in order to catch `Error`s.
|
||||
3. `doPerformAction` attempts to perform the purpose of the action (e.g., send the email) using the supplied
|
||||
data.
|
||||
- `performAction` invokes this method and wraps it in order to catch `Error`s.
|
||||
|
||||
Every method, excluding `getMissingFields`, is asynchronous.
|
||||
|
||||
#### `getMissingFields()`
|
||||
|
||||
This method enables the building of "Sharing"-style UIs that allow the same payload to be shared across
|
||||
many different actions. This is the same approach taken in iOS and Android sharing frameworks.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
action.getMissingFields({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Object[] | The fields that were not present in the `notification` object. Empty array otherwise. |
|
||||
|
||||
The object definition should match:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `field` | String | The JSON field name that was expected. |
|
||||
| `name` | String | The user-readable name of the field (e.g., for a generated UI). |
|
||||
| `type` | String | The type of data (`email`, `text`, `markdown`, `number`, `date`, `boolean`). |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
// Action Users
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const fields = action.getMissingFields({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (fields.length !== 0) {
|
||||
// there's some fields missing so this action should not be used yet
|
||||
}
|
||||
|
||||
// Action Creators
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server, defaults = { } }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(notification) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.to) && !Boolean(notification.to)) {
|
||||
missingFields.push({
|
||||
field: 'to',
|
||||
name: 'To',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### `performHealthCheck()`
|
||||
|
||||
This method enables the health status of third party services to be polled. The current approach only allows
|
||||
an `Action`'s health to be in a boolean state: either it's up and expected to work, or it's down.
|
||||
|
||||
The health check is interesting because some third party services that we anticipate supporting are
|
||||
inherently untrustworthy when it comes to supporting health checks (e.g., email servers). Therefore, it
|
||||
should not be expected that the health check will block usage of actions -- only provide feedback to the
|
||||
user, which we can ideally provide in a future notification center within the UI.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const result = await action.performHealthCheck();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
None.
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const result = await action.performHealthCheck();
|
||||
|
||||
if (result.isOk()) {
|
||||
// theoretically the action is in a usable state
|
||||
} else {
|
||||
// theoretically the action is not in a usable state (but some services may have broken health checks!)
|
||||
}
|
||||
```
|
||||
|
||||
#### `performAction()`
|
||||
|
||||
This method enables the actual usage of the `Action` for action users.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const result = await action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const result = await action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
// theoretically the action is in a usable state
|
||||
} else {
|
||||
// theoretically the action is not in a usable state (but some services may have broken health checks!)
|
||||
}
|
||||
```
|
||||
|
||||
#### `doPerformHealthCheck()`
|
||||
|
||||
This method is for `Action` creators. This performs the actual work to check the health of the action's
|
||||
associated service as best as possible.
|
||||
|
||||
This method should be thought of as a `protected` method only and it should never be called directly
|
||||
outside of tests.
|
||||
|
||||
##### Syntax
|
||||
|
||||
Do not call this method directly outside of tests.
|
||||
|
||||
```js
|
||||
const result = await action.doPerformHealthCheck();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
None.
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method can throw an `Error` in lieu of returning an `ActionResult`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose health status can be checked automatically.
|
||||
|
||||
```js
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
// this responds with a boolean 'true' response, otherwise throws an Error
|
||||
const response = await this.transporter.verify();
|
||||
|
||||
return new ActionResult({
|
||||
message: `Fake action configuration has been verified.`,
|
||||
response: {
|
||||
verified: true
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### `doPerformAction()`
|
||||
|
||||
This method is for `Action` creators. This performs the actual function of the action.
|
||||
|
||||
This method should be thought of as a `protected` method only and it should never be called directly
|
||||
outside of tests.
|
||||
|
||||
##### Syntax
|
||||
|
||||
Do not call this method directly outside of tests.
|
||||
|
||||
```js
|
||||
const result = await action.doPerformAction();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method can throw an `Error` in lieu of returning an `ActionResult`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose health status can be checked automatically.
|
||||
|
||||
```js
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
// Note: This throws an Error upon failure
|
||||
const response = await this.transporter.sendMail({
|
||||
from: notification.from,
|
||||
to: notification.to,
|
||||
cc: notification.cc,
|
||||
bcc: notification.bcc,
|
||||
subject: notification.subject,
|
||||
html: notification.markdown,
|
||||
text: notification.markdown,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Success! Sent email for '${notification.subject}'.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
/**
|
||||
* Actions represent a singular, generic "action", such as "Send to Email".
|
||||
*
|
||||
* Note: Implementations of Action are inherently server-side operations. It may or may not be desiable to fire
|
||||
* these actions from the UI (triggering a server-side call), but these should be called for such a purpose.
|
||||
*/
|
||||
export class Action {
|
||||
|
||||
/**
|
||||
* Create a new Action.
|
||||
*
|
||||
* The suggested ID is the name of the plugin that provides it, and the unique portion.
|
||||
* For example: "core-email" if core provided email.
|
||||
*
|
||||
* @param {Object} server The Kibana server object.
|
||||
* @param {String} id The unique identifier for the action.
|
||||
* @param {String} name User-friendly name for the action.
|
||||
*/
|
||||
constructor({ server, id, name }) {
|
||||
this.server = server;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of the Action.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user-friendly name of the Action.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this action can use the {@code notification}. This is useful if you use the action service
|
||||
* generically, such as using it from a generic UI.
|
||||
*
|
||||
* This is intended to be a simple check of the {@code notification}, rather than an asynchronous action.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Array} Array defining missing fields. Empty if none.
|
||||
*/
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementers must override to perform the health check.
|
||||
*
|
||||
* This should not be called directly outside of tests to ensure that any error is wrapped properly.
|
||||
*
|
||||
* Note: Some services do not provide consistent, reliable health checks, such as email. As such,
|
||||
* implementers must weigh the nature of false negatives versus the utility of having this check.
|
||||
*
|
||||
* @return {Promise} The result of the health check, which must be an {@code ActionResult}.
|
||||
* @throws {Error} if there is an unexpected failure occurs.
|
||||
*/
|
||||
async doPerformHealthCheck() {
|
||||
throw new Error(`[doPerformHealthCheck] is not implemented for '${this.name}' action.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the action can be used to the best of the ability of the service that it is using.
|
||||
*
|
||||
* @return {Promise} Always an {@code ActionResult}.
|
||||
*/
|
||||
async performHealthCheck() {
|
||||
try {
|
||||
return await this.doPerformHealthCheck();
|
||||
} catch (error) {
|
||||
return new ActionResult({
|
||||
message: `Unable to perform '${this.name}' health check: ${error.message}.`,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementers must override to perform the action using the {@code notification}.
|
||||
*
|
||||
* This should not be called directly to ensure that any error is wrapped properly.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Promise} The result of the action, which must be a {@code ActionResult}.
|
||||
* @throws {Error} if the method is not implemented or an unexpected failure occurs.
|
||||
*/
|
||||
async doPerformAction(notification) {
|
||||
throw new Error(`[doPerformAction] is not implemented for '${this.name}' action: ${JSON.stringify(notification)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the current license allows actions.
|
||||
*
|
||||
* @return {Boolean} true when it is usable
|
||||
* @throws {Error} if there is an unexpected issue checking the license
|
||||
*/
|
||||
isLicenseValid() {
|
||||
return this.server.plugins.xpack_main.info.license.isNotBasic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the action using the {@code notification}.
|
||||
*
|
||||
* Actions automatically fail if the license check fails.
|
||||
*
|
||||
* Note to implementers: override doPerformAction instead of this method to help guarantee proper handling.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Promise} The result of the action, which must be a {@code ActionResult}.
|
||||
*/
|
||||
async performAction(notification) {
|
||||
try {
|
||||
if (!this.isLicenseValid()) {
|
||||
throw new Error(`The current license does not allow '${this.name}' action.`);
|
||||
}
|
||||
|
||||
return await this.doPerformAction(notification);
|
||||
} catch (error) {
|
||||
return new ActionResult({
|
||||
message: `Unable to perform '${this.name}' action: ${error.message}.`,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Action } from './action';
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
describe('Action', () => {
|
||||
|
||||
const server = { };
|
||||
const id = 'notifications-test';
|
||||
const unimplementedName = 'Unimplemented';
|
||||
const throwsErrorName = 'Throws Error';
|
||||
const passThruName = 'Test Action';
|
||||
const action = new Action({ server, id, name: unimplementedName });
|
||||
const notification = {
|
||||
fake: true,
|
||||
};
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.server).toBe(server);
|
||||
expect(action.getId()).toBe(id);
|
||||
expect(action.getName()).toBe(unimplementedName);
|
||||
});
|
||||
|
||||
test('getMissingFields returns an empty array', () => {
|
||||
expect(action.getMissingFields()).toEqual([]);
|
||||
expect(action.getMissingFields(notification)).toEqual([]);
|
||||
});
|
||||
|
||||
test('doPerformHealthChecks throws error indicating that it is not implemented', async () => {
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(`[doPerformHealthCheck] is not implemented for '${unimplementedName}' action.`);
|
||||
});
|
||||
|
||||
describe('performHealthChecks', () => {
|
||||
|
||||
class ThrowsErrorHealthCheckAction extends Action {
|
||||
constructor() {
|
||||
super({ server: { }, id, name: throwsErrorName });
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
throw new Error('TEST - expected');
|
||||
}
|
||||
}
|
||||
|
||||
class PassThruHealthCheckAction extends Action {
|
||||
constructor(result) {
|
||||
super({ server: { }, id, name: passThruName });
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
test('runs against unimplemented doPerformHealthChecks', async () => {
|
||||
const result = await action.performHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${unimplementedName}' health check: \\[doPerformHealthCheck\\] is not.*`));
|
||||
});
|
||||
|
||||
test('runs against failing doPerformHealthChecks', async () => {
|
||||
const failAction = new ThrowsErrorHealthCheckAction();
|
||||
const result = await failAction.performHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${throwsErrorName}' health check: TEST - expected`));
|
||||
});
|
||||
|
||||
test('runs against succeeding result', async () => {
|
||||
const expectedResult = new ActionResult({ message: 'Blah', response: { ok: true } });
|
||||
const succeedsAction = new PassThruHealthCheckAction(expectedResult);
|
||||
const result = await succeedsAction.performHealthCheck();
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('doPerformAction throws error indicating that it is not implemented', async () => {
|
||||
await expect(action.doPerformAction(notification))
|
||||
.rejects
|
||||
.toThrow(`[doPerformAction] is not implemented for '${unimplementedName}' action: {"fake":true}`);
|
||||
});
|
||||
|
||||
describe('isLicenseValid', () => {
|
||||
|
||||
test('server variable is not exposed as expected', () => {
|
||||
expect(() => action.isLicenseValid()).toThrow(Error);
|
||||
});
|
||||
|
||||
test('returns false is license is not valid', () => {
|
||||
const unlicensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const unlicensedAction = new Action({ server: unlicensedServer, id, name: unimplementedName });
|
||||
|
||||
expect(unlicensedAction.isLicenseValid()).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true is license is not valid', () => {
|
||||
const licensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const licensedAction = new Action({ server: licensedServer, id, name: unimplementedName });
|
||||
|
||||
expect(licensedAction.isLicenseValid()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('performAction', () => {
|
||||
|
||||
// valid license
|
||||
const server = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ThrowsErrorAction extends Action {
|
||||
constructor() {
|
||||
super({ server, id, name: throwsErrorName });
|
||||
}
|
||||
|
||||
async doPerformAction() {
|
||||
throw new Error('TEST - expected');
|
||||
}
|
||||
}
|
||||
|
||||
class PassThruAction extends Action {
|
||||
constructor(result) {
|
||||
super({ server, id, name: passThruName });
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
async doPerformAction() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('fails license check', () => {
|
||||
|
||||
// handles the case when xpack_main definitions change
|
||||
test('because of bad reference', async () => {
|
||||
// server is an empty object, so a reference fails early in the chain (mostly a way to give devs a way to find this error)
|
||||
const result = await action.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
});
|
||||
|
||||
test('because license is invalid or basic', async () => {
|
||||
const unlicensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const unlicensedAction = new Action({ server: unlicensedServer, id, name: unimplementedName });
|
||||
const result = await unlicensedAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(
|
||||
`Unable to perform '${unimplementedName}' action: ` +
|
||||
`The current license does not allow '${unimplementedName}' action.`
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('runs against unimplemented doPerformAction', async () => {
|
||||
const licensedAction = new Action({ server, id, name: unimplementedName });
|
||||
const result = await licensedAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${unimplementedName}' action: \\[doPerformAction\\] is not.*`));
|
||||
});
|
||||
|
||||
test('runs against failing doPerformAction', async () => {
|
||||
const failAction = new ThrowsErrorAction();
|
||||
const result = await failAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${throwsErrorName}' action: TEST - expected`));
|
||||
});
|
||||
|
||||
test('runs against succeeding result', async () => {
|
||||
const expectedResult = new ActionResult({ message: 'Blah', response: { ok: true } });
|
||||
const succeedsAction = new PassThruAction(expectedResult);
|
||||
const result = await succeedsAction.performAction(notification);
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Action Results represent generic, predictable responses from Actions.
|
||||
*/
|
||||
export class ActionResult {
|
||||
|
||||
/**
|
||||
* Create a new Action Result.
|
||||
*
|
||||
* Success is determined by the existence of an error.
|
||||
*
|
||||
* @param {String} message The message to display about the result, presumably in a Toast.
|
||||
* @param {Object|undefined} response The response from the "other" side.
|
||||
* @param {Object|undefined} error The error, if any.
|
||||
*/
|
||||
constructor({ message, response, error }) {
|
||||
this.message = message;
|
||||
this.response = response;
|
||||
this.error = error;
|
||||
this.ok = !Boolean(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error caused by the action.
|
||||
*
|
||||
* @returns {Object|undefined} The error response, or {@code undefined} if no error.
|
||||
*/
|
||||
getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message displayable to the user.
|
||||
*
|
||||
* @returns {String} The message.
|
||||
*/
|
||||
getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw JSON response from the action.
|
||||
*
|
||||
* @returns {Object|undefined} The JSON response.
|
||||
*/
|
||||
getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the action succeeded.
|
||||
*
|
||||
* @returns {Boolean} {@code true} for success.
|
||||
*/
|
||||
isOk() {
|
||||
return this.ok;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
ok: this.ok,
|
||||
error: this.error,
|
||||
message: this.message,
|
||||
response: this.response,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
describe('ActionResult', () => {
|
||||
|
||||
const message = 'this is a message';
|
||||
const response = { other: { side: { response: true } } };
|
||||
const error = { message: `Error: ${message}` };
|
||||
|
||||
const okResult = new ActionResult({ message, response });
|
||||
const notOkResult = new ActionResult({ message, response, error });
|
||||
|
||||
test('getError returns supplied error or undefined', () => {
|
||||
expect(okResult.getError()).toBeUndefined();
|
||||
expect(notOkResult.getError()).toBe(error);
|
||||
});
|
||||
|
||||
test('getMessage returns supplied message', () => {
|
||||
expect(okResult.getMessage()).toBe(message);
|
||||
expect(notOkResult.getMessage()).toBe(message);
|
||||
});
|
||||
|
||||
test('getResponse returns supplied response', () => {
|
||||
expect(okResult.getResponse()).toBe(response);
|
||||
expect(notOkResult.getResponse()).toBe(response);
|
||||
});
|
||||
|
||||
test('isOk returns based on having an error', () => {
|
||||
expect(okResult.isOk()).toBe(true);
|
||||
expect(notOkResult.isOk()).toBe(false);
|
||||
});
|
||||
|
||||
test('toJson', () => {
|
||||
expect(okResult.toJson()).toEqual({
|
||||
ok: true,
|
||||
error: undefined,
|
||||
message,
|
||||
response,
|
||||
});
|
||||
|
||||
expect(notOkResult.toJson()).toEqual({
|
||||
ok: false,
|
||||
error,
|
||||
message,
|
||||
response,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { notificationService } from './notification_service';
|
||||
export { Action } from './action';
|
||||
export { ActionResult } from './action_result';
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Notification Service represents a service that contains generic "actions", such as "Send to Email"
|
||||
* that are added at startup by plugins to enable notifications to be sent either by the user manually
|
||||
* or via the some scheduled / automated task.
|
||||
*/
|
||||
export class NotificationService {
|
||||
|
||||
constructor() {
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new action to the action service.
|
||||
*
|
||||
* @param {Action} action An implementation of Action.
|
||||
*/
|
||||
setAction = (action) => {
|
||||
this.removeAction(action.id);
|
||||
|
||||
this.actions.push(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing action from the action service.
|
||||
*
|
||||
* @param {String} id The ID of the action to remove.
|
||||
* @return {Action} The action that was removed, or null.
|
||||
*/
|
||||
removeAction = (id) => {
|
||||
const index = this.actions.findIndex(action => action.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
const removedActions = this.actions.splice(index, 1);
|
||||
return removedActions[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action with the specified {@code id}, if any.
|
||||
*
|
||||
* This is useful when you know that an action is provided, such as one provided by your own plugin,
|
||||
* and you want to use it to handle things in a consistent way.
|
||||
*
|
||||
* @param {String} id The ID of the Action.
|
||||
* @return {Action} The Action that matches the ID, or null.
|
||||
*/
|
||||
getActionForId = (id) => {
|
||||
const index = this.actions.findIndex(action => action.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
return this.actions[index];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actions that will accept the {@code data}.
|
||||
*
|
||||
* @param {Object} data The data object to pass to actions.
|
||||
* @return {Array} An array of Actions that can be used with the data, if any. Empty if none.
|
||||
*/
|
||||
getActionsForData = (data) => {
|
||||
return this.actions.filter(action => {
|
||||
try {
|
||||
return action.getMissingFields(data).length === 0;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton reference to the notification service intended to be used across Kibana.
|
||||
*/
|
||||
export const notificationService = new NotificationService();
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Action } from './action';
|
||||
import { NotificationService } from './notification_service';
|
||||
|
||||
class TestAction extends Action {
|
||||
constructor({ server, id }) {
|
||||
super({ server, id, name: 'TestAction' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// always returns a missing field
|
||||
class MissingFieldTestAction extends Action {
|
||||
constructor({ server, id }) {
|
||||
super({ server, id, name: 'MissingFieldTestAction' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [ { field: 'subject', name: 'Subject', type: 'text' } ];
|
||||
}
|
||||
}
|
||||
|
||||
describe('NotificationService', () => {
|
||||
|
||||
const server = { };
|
||||
const actionId = 'notifications-test';
|
||||
const action = new TestAction({ server, id: actionId });
|
||||
|
||||
let notificationService;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationService = new NotificationService();
|
||||
});
|
||||
|
||||
test('initializes with no default actions', () => {
|
||||
expect(notificationService.actions).toEqual([]);
|
||||
});
|
||||
|
||||
describe('setAction', () => {
|
||||
|
||||
test('adds the action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('removes any action with the same ID first, then adds the action', () => {
|
||||
notificationService.setAction({ id: actionId });
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.actions).toHaveLength(1);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('removeAction', () => {
|
||||
|
||||
test('returns null if the action does not exist', () => {
|
||||
expect(notificationService.removeAction(actionId)).toBe(null);
|
||||
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.removeAction('not-' + actionId)).toBe(null);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('returns the removed action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.removeAction(actionId)).toBe(action);
|
||||
expect(notificationService.actions).toEqual([]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getActionForId', () => {
|
||||
|
||||
test('returns null if the action does not exist', () => {
|
||||
expect(notificationService.getActionForId(actionId)).toBe(null);
|
||||
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionForId('not-' + actionId)).toBe(null);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('returns the action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionForId(actionId)).toBe(action);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getActionsForData', () => {
|
||||
|
||||
test('returns [] if no corresponding action exists', () => {
|
||||
expect(notificationService.getActionsForData({})).toEqual([]);
|
||||
|
||||
notificationService.setAction(new MissingFieldTestAction({ server, id: 'always-missing' }));
|
||||
|
||||
expect(notificationService.getActionsForData({})).toEqual([]);
|
||||
expect(notificationService.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('returns the actions that match', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionsForData({})).toEqual([ action ]);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
|
||||
const otherActionId = 'other-' + actionId;
|
||||
|
||||
notificationService.setAction(new MissingFieldTestAction({ server, id: 'always-missing' }));
|
||||
notificationService.setAction(new TestAction({ server, id: otherActionId }));
|
||||
|
||||
const actions = notificationService.getActionsForData({});
|
||||
|
||||
expect(actions.map(action => action.id)).toEqual([ actionId, otherActionId ]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { SlackAction } from './slack_action';
|
||||
|
||||
/**
|
||||
* Create a Slack options object from the config.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Slack.
|
||||
*/
|
||||
export function optionsFromConfig(config) {
|
||||
return {
|
||||
token: config.get('xpack.notifications.slack.token')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Slack defaults object from the config.
|
||||
*
|
||||
* Defaults include things like the default channel that messages are posted to.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Slack on a per-message basis.
|
||||
*/
|
||||
export function defaultsFromConfig(config) {
|
||||
return {
|
||||
channel: config.get('xpack.notifications.slack.defaults.channel'),
|
||||
as_user: config.get('xpack.notifications.slack.defaults.as_user'),
|
||||
icon_emoji: config.get('xpack.notifications.slack.defaults.icon_emoji'),
|
||||
icon_url: config.get('xpack.notifications.slack.defaults.icon_url'),
|
||||
link_names: config.get('xpack.notifications.slack.defaults.link_names'),
|
||||
mrkdwn: config.get('xpack.notifications.slack.defaults.mrkdwn'),
|
||||
unfurl_links: config.get('xpack.notifications.slack.defaults.unfurl_links'),
|
||||
unfurl_media: config.get('xpack.notifications.slack.defaults.unfurl_media'),
|
||||
username: config.get('xpack.notifications.slack.defaults.username'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Slack Action based on the configuration.
|
||||
*
|
||||
* @param {Object} server The server object.
|
||||
* @return {SlackAction} A new Slack Action based on the kibana.yml configuration.
|
||||
*/
|
||||
export function createSlackAction(server, { _options = optionsFromConfig, _defaults = defaultsFromConfig } = { }) {
|
||||
const config = server.config();
|
||||
|
||||
const options = _options(config);
|
||||
const defaults = _defaults(config);
|
||||
|
||||
return new SlackAction({ server, options, defaults });
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { SlackAction } from './slack_action';
|
||||
import {
|
||||
createSlackAction,
|
||||
defaultsFromConfig,
|
||||
optionsFromConfig,
|
||||
} from './create_slack_action';
|
||||
|
||||
describe('create_slack_action', () => {
|
||||
|
||||
test('optionsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'token',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.slack.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(optionsFromConfig({ get })).toEqual({
|
||||
token: 'token',
|
||||
});
|
||||
});
|
||||
|
||||
test('defaultsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'channel',
|
||||
'as_user',
|
||||
'icon_emoji',
|
||||
'icon_url',
|
||||
'link_names',
|
||||
'mrkdwn',
|
||||
'unfurl_links',
|
||||
'unfurl_media',
|
||||
'username',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.slack.defaults.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(defaultsFromConfig({ get })).toEqual({
|
||||
channel: 'channel',
|
||||
as_user: 'as_user',
|
||||
icon_emoji: 'icon_emoji',
|
||||
icon_url: 'icon_url',
|
||||
link_names: 'link_names',
|
||||
mrkdwn: 'mrkdwn',
|
||||
unfurl_links: 'unfurl_links',
|
||||
unfurl_media: 'unfurl_media',
|
||||
username: 'username',
|
||||
});
|
||||
});
|
||||
|
||||
test('createSlackAction', async () => {
|
||||
const config = { };
|
||||
const server = { config: jest.fn().mockReturnValue(config) };
|
||||
const _options = jest.fn().mockReturnValue({ options: true });
|
||||
const defaults = { defaults: true };
|
||||
const _defaults = jest.fn().mockReturnValue(defaults);
|
||||
|
||||
const action = createSlackAction(server, { _options, _defaults });
|
||||
|
||||
expect(action instanceof SlackAction).toBe(true);
|
||||
expect(action.defaults).toBe(defaults);
|
||||
|
||||
expect(server.config).toHaveBeenCalledTimes(1);
|
||||
expect(server.config).toHaveBeenCalledWith();
|
||||
expect(_options).toHaveBeenCalledTimes(1);
|
||||
expect(_options).toHaveBeenCalledWith(config);
|
||||
expect(_defaults).toHaveBeenCalledTimes(1);
|
||||
expect(_defaults).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createSlackAction } from './create_slack_action';
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { WebClient } from '@slack/client';
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const SLACK_ACTION_ID = 'xpack-notifications-slack';
|
||||
|
||||
/**
|
||||
* Create a new Slack {@code WebClient}.
|
||||
*
|
||||
* Currently the only option expected is {@code token}.
|
||||
*
|
||||
* @param {Object} options Slack API options.
|
||||
* @returns {WebClient} Always.
|
||||
*/
|
||||
export function webClientCreator(options) {
|
||||
return new WebClient(options.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack Action enables generic sending of Slack messages.
|
||||
*/
|
||||
export class SlackAction extends Action {
|
||||
|
||||
/**
|
||||
* Create a new Action capable of sending Slack messages.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {Object} options Configuration options for the Slack WebClient. Currently only expect "token" field.
|
||||
* @param {Object} defaults Default fields used when sending messages.
|
||||
* @param {Function} _webClientCreator Exposed for tests.
|
||||
*/
|
||||
constructor({ server, options, defaults = { }, _webClientCreator = webClientCreator }) {
|
||||
super({ server, id: SLACK_ACTION_ID, name: 'Slack' });
|
||||
|
||||
this.client = _webClientCreator(options);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(data) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.channel) && !Boolean(data.channel)) {
|
||||
missingFields.push({
|
||||
field: 'channel',
|
||||
name: 'Channel',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(data.subject)) {
|
||||
missingFields.push({
|
||||
field: 'subject',
|
||||
name: 'Message',
|
||||
type: 'markdown',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
const response = await this.client.api.test();
|
||||
|
||||
if (response.ok) {
|
||||
return new ActionResult({
|
||||
message: `Slack action configuration has been verified.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return new ActionResult({
|
||||
message: `Slack action configuration could not be verified.`,
|
||||
response,
|
||||
error: response.error || { message: 'Unknown Error' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the message based on whether or not a {@code markdown} body was supplied.
|
||||
*/
|
||||
renderMessage({ subject, markdown }) {
|
||||
const attachments = [];
|
||||
|
||||
if (markdown) {
|
||||
attachments.push({ text: markdown });
|
||||
}
|
||||
|
||||
return { text: subject, attachments };
|
||||
}
|
||||
|
||||
async doPerformAction({ subject, markdown, channel }) {
|
||||
// NOTE: When we want to support files, then we should look into using client.files.upload({ ... })
|
||||
// without _also_ sending chat message because the file upload endpoint supports chat behavior
|
||||
// in addition to files, but the reverse is not true.
|
||||
const slackChannel = channel || this.defaults.channel;
|
||||
|
||||
const response = await this.client.chat.postMessage({
|
||||
...this.defaults,
|
||||
...this.renderMessage({ subject, markdown }),
|
||||
channel: slackChannel,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Posted Slack message to channel '${slackChannel}'.`,
|
||||
response,
|
||||
error: response.error,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,277 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { WebClient } from '@slack/client';
|
||||
import { ActionResult } from '../';
|
||||
import {
|
||||
SLACK_ACTION_ID,
|
||||
SlackAction,
|
||||
webClientCreator,
|
||||
} from './slack_action';
|
||||
|
||||
describe('SlackAction', () => {
|
||||
|
||||
const server = { };
|
||||
const options = { options: true };
|
||||
const defaults = { defaults: true };
|
||||
const client = {
|
||||
api: {
|
||||
// see beforeEach
|
||||
},
|
||||
chat: {
|
||||
// see beforeEach
|
||||
}
|
||||
};
|
||||
let _webClientCreator;
|
||||
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
client.api.test = jest.fn();
|
||||
client.chat.postMessage = jest.fn();
|
||||
_webClientCreator = jest.fn().mockReturnValue(client);
|
||||
|
||||
action = new SlackAction({ server, options, defaults, _webClientCreator });
|
||||
});
|
||||
|
||||
test('webClientCreator creates a WebClient', () => {
|
||||
expect(webClientCreator('faketoken') instanceof WebClient).toBe(true);
|
||||
});
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.getId()).toBe(SLACK_ACTION_ID);
|
||||
expect(action.getName()).toBe('Slack');
|
||||
expect(action.client).toBe(client);
|
||||
|
||||
expect(_webClientCreator).toHaveBeenCalledTimes(1);
|
||||
expect(_webClientCreator).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
describe('getMissingFields', () => {
|
||||
|
||||
test('returns missing fields', () => {
|
||||
const channel = { field: 'channel', name: 'Channel', type: 'text' };
|
||||
const subject = { field: 'subject', name: 'Message', type: 'markdown' };
|
||||
|
||||
const missing = [
|
||||
{ defaults: { }, notification: { }, missing: [ channel, subject, ], },
|
||||
{ defaults: { }, notification: { channel: '#kibana', }, missing: [ subject, ], },
|
||||
{ defaults: { channel: '#kibana', }, notification: { }, missing: [ subject, ], },
|
||||
{ defaults: { }, notification: { subject: 'subject', }, missing: [ channel, ], },
|
||||
];
|
||||
|
||||
missing.forEach(check => {
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: check.defaults, _webClientCreator });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual(check.missing);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns [] when all fields exist', () => {
|
||||
const exists = [
|
||||
{ defaults: { }, notification: { channel: '#kibana', subject: 'subject', }, },
|
||||
{ defaults: { channel: '#kibana', }, notification: { subject: 'subject', }, },
|
||||
];
|
||||
|
||||
exists.forEach(check => {
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: check.defaults, _webClientCreator });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformHealthCheck', () => {
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
client.api.test.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult if not ok with error', async () => {
|
||||
const response = { ok: false, error: { expected: true } };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration could not be verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toBe(response.error);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult if not ok with default error', async () => {
|
||||
const response = { ok: false };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration could not be verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toEqual({ message: 'Unknown Error' });
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { ok: true };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration has been verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('renderMessage', () => {
|
||||
|
||||
test('does not contain attachments', () => {
|
||||
const message = { subject: 'subject' };
|
||||
const response = action.renderMessage(message);
|
||||
|
||||
expect(response).toMatchObject({
|
||||
text: message.subject,
|
||||
attachments: [ ]
|
||||
});
|
||||
});
|
||||
|
||||
test('contains attachments', () => {
|
||||
const message = { subject: 'subject', markdown: 'markdown' };
|
||||
const response = action.renderMessage(message);
|
||||
|
||||
expect(response).toMatchObject({
|
||||
text: message.subject,
|
||||
attachments: [
|
||||
{
|
||||
text: message.markdown
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformAction', () => {
|
||||
const message = { channel: '#kibana', subject: 'subject', markdown: 'body', };
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
client.chat.postMessage.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformAction(message))
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for failure without Error', async () => {
|
||||
const response = { fake: true, error: { expected: true } };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(message);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${message.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toBe(response.error);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { fake: true };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(message);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${message.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for success with default channel', async () => {
|
||||
const response = { fake: false };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const channelDefaults = {
|
||||
...defaults,
|
||||
channel: '#kibana',
|
||||
};
|
||||
const noChannelMessage = {
|
||||
...message,
|
||||
channel: undefined,
|
||||
};
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: channelDefaults, _webClientCreator });
|
||||
|
||||
const result = await newDefaultsAction.doPerformAction(noChannelMessage);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${channelDefaults.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(noChannelMessage),
|
||||
channel: channelDefaults.channel,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue