From 6419c232e5c57089ab08ff9307c41119e84600c5 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Mon, 22 Jul 2019 14:19:20 -0400 Subject: [PATCH] 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. --- x-pack/index.js | 2 - x-pack/legacy/plugins/notifications/config.js | 51 -- x-pack/legacy/plugins/notifications/index.js | 24 - x-pack/legacy/plugins/notifications/init.js | 38 -- .../server/email/create_email_action.js | 58 -- .../server/email/create_email_action.test.js | 96 --- .../server/email/email_action.js | 103 ---- .../server/email/email_action.test.js | 166 ----- .../notifications/server/email/index.js | 7 - .../plugins/notifications/server/index.js | 14 - .../notifications/server/logger/index.js | 7 - .../server/logger/logger_action.js | 42 -- .../server/logger/logger_action.test.js | 50 -- .../routes/api/v1/notifications/index.js | 7 - .../routes/api/v1/notifications/notify.js | 107 ---- .../api/v1/notifications/notify.test.js | 164 ----- .../notifications/server/service/README.md | 575 ------------------ .../notifications/server/service/action.js | 143 ----- .../server/service/action.test.js | 242 -------- .../server/service/action_result.js | 73 --- .../server/service/action_result.test.js | 54 -- .../notifications/server/service/index.js | 9 - .../server/service/notification_service.js | 86 --- .../service/notification_service.test.js | 134 ---- .../server/slack/create_slack_action.js | 56 -- .../server/slack/create_slack_action.test.js | 94 --- .../notifications/server/slack/index.js | 7 - .../server/slack/slack_action.js | 116 ---- .../server/slack/slack_action.test.js | 277 --------- 29 files changed, 2802 deletions(-) delete mode 100644 x-pack/legacy/plugins/notifications/config.js delete mode 100644 x-pack/legacy/plugins/notifications/index.js delete mode 100644 x-pack/legacy/plugins/notifications/init.js delete mode 100644 x-pack/legacy/plugins/notifications/server/email/create_email_action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/email/create_email_action.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/email/email_action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/email/email_action.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/email/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/logger/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/logger/logger_action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/logger/logger_action.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.js delete mode 100644 x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/README.md delete mode 100644 x-pack/legacy/plugins/notifications/server/service/action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/action.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/action_result.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/action_result.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/notification_service.js delete mode 100644 x-pack/legacy/plugins/notifications/server/service/notification_service.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/slack/create_slack_action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/slack/create_slack_action.test.js delete mode 100644 x-pack/legacy/plugins/notifications/server/slack/index.js delete mode 100644 x-pack/legacy/plugins/notifications/server/slack/slack_action.js delete mode 100644 x-pack/legacy/plugins/notifications/server/slack/slack_action.test.js diff --git a/x-pack/index.js b/x-pack/index.js index c035d13ceaebcc..9b62fc18616f78 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -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), diff --git a/x-pack/legacy/plugins/notifications/config.js b/x-pack/legacy/plugins/notifications/config.js deleted file mode 100644 index 996db49542646c..00000000000000 --- a/x-pack/legacy/plugins/notifications/config.js +++ /dev/null @@ -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(); -}; diff --git a/x-pack/legacy/plugins/notifications/index.js b/x-pack/legacy/plugins/notifications/index.js deleted file mode 100644 index e6de33e9685e0e..00000000000000 --- a/x-pack/legacy/plugins/notifications/index.js +++ /dev/null @@ -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, -}); diff --git a/x-pack/legacy/plugins/notifications/init.js b/x-pack/legacy/plugins/notifications/init.js deleted file mode 100644 index 67c34b336f7373..00000000000000 --- a/x-pack/legacy/plugins/notifications/init.js +++ /dev/null @@ -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); -} diff --git a/x-pack/legacy/plugins/notifications/server/email/create_email_action.js b/x-pack/legacy/plugins/notifications/server/email/create_email_action.js deleted file mode 100644 index 5e3b63e4b7cd36..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/email/create_email_action.js +++ /dev/null @@ -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 }); -} diff --git a/x-pack/legacy/plugins/notifications/server/email/create_email_action.test.js b/x-pack/legacy/plugins/notifications/server/email/create_email_action.test.js deleted file mode 100644 index fb9d46cdbe71ef..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/email/create_email_action.test.js +++ /dev/null @@ -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); - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/email/email_action.js b/x-pack/legacy/plugins/notifications/server/email/email_action.js deleted file mode 100644 index a30b88a798d272..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/email/email_action.js +++ /dev/null @@ -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, - }); - } - -} diff --git a/x-pack/legacy/plugins/notifications/server/email/email_action.test.js b/x-pack/legacy/plugins/notifications/server/email/email_action.test.js deleted file mode 100644 index 90ef5a3f59a780..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/email/email_action.test.js +++ /dev/null @@ -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, - }); - }); - - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/email/index.js b/x-pack/legacy/plugins/notifications/server/email/index.js deleted file mode 100644 index ff784e71bab738..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/email/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/index.js b/x-pack/legacy/plugins/notifications/server/index.js deleted file mode 100644 index d877b5bb704301..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/logger/index.js b/x-pack/legacy/plugins/notifications/server/logger/index.js deleted file mode 100644 index c9d4686ad4ef38..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/logger/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/logger/logger_action.js b/x-pack/legacy/plugins/notifications/server/logger/logger_action.js deleted file mode 100644 index 189f3638677cbc..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/logger/logger_action.js +++ /dev/null @@ -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 - }); - } - -} diff --git a/x-pack/legacy/plugins/notifications/server/logger/logger_action.test.js b/x-pack/legacy/plugins/notifications/server/logger/logger_action.test.js deleted file mode 100644 index 894399c0dc8bdc..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/logger/logger_action.test.js +++ /dev/null @@ -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); - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/index.js b/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/index.js deleted file mode 100644 index dd754c454ee254..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.js b/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.js deleted file mode 100644 index 88934583d500e2..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.js +++ /dev/null @@ -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); - }, - }); -} diff --git a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.test.js b/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.test.js deleted file mode 100644 index 0a50ef9cc1ba4b..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/routes/api/v1/notifications/notify.test.js +++ /dev/null @@ -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)); - }); - - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/service/README.md b/x-pack/legacy/plugins/notifications/server/service/README.md deleted file mode 100644 index 1d9a2f2889a3c9..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/README.md +++ /dev/null @@ -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 ... - -} -``` diff --git a/x-pack/legacy/plugins/notifications/server/service/action.js b/x-pack/legacy/plugins/notifications/server/service/action.js deleted file mode 100644 index 6077ff6e2b41a9..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/action.js +++ /dev/null @@ -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 - }); - } - } - -} diff --git a/x-pack/legacy/plugins/notifications/server/service/action.test.js b/x-pack/legacy/plugins/notifications/server/service/action.test.js deleted file mode 100644 index a6fa5b28131318..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/action.test.js +++ /dev/null @@ -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); - }); - - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/service/action_result.js b/x-pack/legacy/plugins/notifications/server/service/action_result.js deleted file mode 100644 index 35507045c898ce..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/action_result.js +++ /dev/null @@ -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, - }; - } - -} diff --git a/x-pack/legacy/plugins/notifications/server/service/action_result.test.js b/x-pack/legacy/plugins/notifications/server/service/action_result.test.js deleted file mode 100644 index 128ddb1dd8b92f..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/action_result.test.js +++ /dev/null @@ -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, - }); - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/service/index.js b/x-pack/legacy/plugins/notifications/server/service/index.js deleted file mode 100644 index dd64e16abc7135..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/service/notification_service.js b/x-pack/legacy/plugins/notifications/server/service/notification_service.js deleted file mode 100644 index 5804532d8be68f..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/notification_service.js +++ /dev/null @@ -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(); diff --git a/x-pack/legacy/plugins/notifications/server/service/notification_service.test.js b/x-pack/legacy/plugins/notifications/server/service/notification_service.test.js deleted file mode 100644 index 903d7ea0c5d922..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/service/notification_service.test.js +++ /dev/null @@ -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 ]); - }); - - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.js b/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.js deleted file mode 100644 index ecffcadc9b4dac..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.js +++ /dev/null @@ -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 }); -} diff --git a/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.test.js b/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.test.js deleted file mode 100644 index 68d669985568a9..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/slack/create_slack_action.test.js +++ /dev/null @@ -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); - }); - -}); diff --git a/x-pack/legacy/plugins/notifications/server/slack/index.js b/x-pack/legacy/plugins/notifications/server/slack/index.js deleted file mode 100644 index da7cd204e88877..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/slack/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/legacy/plugins/notifications/server/slack/slack_action.js b/x-pack/legacy/plugins/notifications/server/slack/slack_action.js deleted file mode 100644 index 236ce1af9c360b..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/slack/slack_action.js +++ /dev/null @@ -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, - }); - } - -} diff --git a/x-pack/legacy/plugins/notifications/server/slack/slack_action.test.js b/x-pack/legacy/plugins/notifications/server/slack/slack_action.test.js deleted file mode 100644 index 833047a580abb7..00000000000000 --- a/x-pack/legacy/plugins/notifications/server/slack/slack_action.test.js +++ /dev/null @@ -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, - }); - }); - - }); - -});