mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Revert "RAM Slack connector improvements" (#154093)
Reverts elastic/kibana#149127 from pmuellr: After looking into https://github.com/elastic/kibana/issues/153939 - Preconfigured slack Web API connector does not work - we realized we really should have added the support for the Slack Web API in a different connector. Lesson learned: don't design connectors where the set of parameters differs depending on a config (or secret) value. We have code - at least with the "test" functionality - that assumes you can create a form of parameters based solely on the connector type, without having access to the connector data (we could/should probably add an enhancement to do that). So it would never be able to render the appropriate parameters. There is also the semantic issue that if you changed the Slack type (from webhook to web-api), via the HTTP API (prevented in the UX), you would break all actions using that connector, since they wouldn't have the right parameters set. Complete guess, but there may be other lurking bits like this in the codebase that we just haven't hit yet. Given all that, we reluctantly decided to split the connector into two, and revert the PR that added the new code. RIP Slack connector improvements, hope to see your new version in a few days! :-)
This commit is contained in:
parent
0f3b37b63c
commit
7776dfa696
40 changed files with 815 additions and 2953 deletions
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const SLACK_CONNECTOR_ID = '.slack';
|
||||
export const SLACK_URL = 'https://slack.com/api/';
|
|
@ -1,49 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const SlackConfigSchema = schema.object({
|
||||
type: schema.oneOf([schema.literal('webhook'), schema.literal('web_api')], {
|
||||
defaultValue: 'webhook',
|
||||
}),
|
||||
});
|
||||
|
||||
export const SlackWebhookSecretsSchema = schema.object({
|
||||
webhookUrl: schema.string({ minLength: 1 }),
|
||||
});
|
||||
export const SlackWebApiSecretsSchema = schema.object({
|
||||
token: schema.string({ minLength: 1 }),
|
||||
});
|
||||
|
||||
export const SlackSecretsSchema = schema.oneOf([
|
||||
SlackWebhookSecretsSchema,
|
||||
SlackWebApiSecretsSchema,
|
||||
]);
|
||||
|
||||
export const ExecutorGetChannelsParamsSchema = schema.object({
|
||||
subAction: schema.literal('getChannels'),
|
||||
});
|
||||
|
||||
export const PostMessageSubActionParamsSchema = schema.object({
|
||||
channels: schema.arrayOf(schema.string()),
|
||||
text: schema.string(),
|
||||
});
|
||||
export const ExecutorPostMessageParamsSchema = schema.object({
|
||||
subAction: schema.literal('postMessage'),
|
||||
subActionParams: PostMessageSubActionParamsSchema,
|
||||
});
|
||||
|
||||
export const WebhookParamsSchema = schema.object({
|
||||
message: schema.string({ minLength: 1 }),
|
||||
});
|
||||
export const WebApiParamsSchema = schema.oneOf([
|
||||
ExecutorGetChannelsParamsSchema,
|
||||
ExecutorPostMessageParamsSchema,
|
||||
]);
|
||||
|
||||
export const SlackParamsSchema = schema.oneOf([WebhookParamsSchema, WebApiParamsSchema]);
|
|
@ -1,57 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import type { ActionTypeExecutorOptions as ConnectorTypeExecutorOptions } from '@kbn/actions-plugin/server/types';
|
||||
import type { ActionType as ConnectorType } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ExecutorPostMessageParamsSchema,
|
||||
PostMessageSubActionParamsSchema,
|
||||
SlackConfigSchema,
|
||||
SlackSecretsSchema,
|
||||
SlackWebhookSecretsSchema,
|
||||
SlackWebApiSecretsSchema,
|
||||
WebhookParamsSchema,
|
||||
WebApiParamsSchema,
|
||||
} from './schema';
|
||||
|
||||
export type SlackConfig = TypeOf<typeof SlackConfigSchema>;
|
||||
export type SlackSecrets = TypeOf<typeof SlackSecretsSchema>;
|
||||
|
||||
export type PostMessageParams = TypeOf<typeof ExecutorPostMessageParamsSchema>;
|
||||
export type PostMessageSubActionParams = TypeOf<typeof PostMessageSubActionParamsSchema>;
|
||||
|
||||
export type SlackWebhookSecrets = TypeOf<typeof SlackWebhookSecretsSchema>;
|
||||
export type SlackWebApiSecrets = TypeOf<typeof SlackWebApiSecretsSchema>;
|
||||
|
||||
export type SlackWebhookExecutorOptions = ConnectorTypeExecutorOptions<
|
||||
SlackConfig,
|
||||
SlackWebhookSecrets,
|
||||
WebhookParams
|
||||
>;
|
||||
export type SlackWebApiExecutorOptions = ConnectorTypeExecutorOptions<
|
||||
SlackConfig,
|
||||
SlackWebApiSecrets,
|
||||
WebApiParams
|
||||
>;
|
||||
|
||||
export type SlackExecutorOptions = ConnectorTypeExecutorOptions<
|
||||
SlackConfig,
|
||||
SlackSecrets,
|
||||
WebhookParams | WebApiParams
|
||||
>;
|
||||
|
||||
export type SlackConnectorType = ConnectorType<
|
||||
SlackConfig,
|
||||
SlackSecrets,
|
||||
WebhookParams | WebApiParams,
|
||||
unknown
|
||||
>;
|
||||
|
||||
export type WebhookParams = TypeOf<typeof WebhookParamsSchema>;
|
||||
export type WebApiParams = TypeOf<typeof WebApiParamsSchema>;
|
||||
export type SlackActionParams = WebhookParams | WebApiParams;
|
|
@ -30,16 +30,13 @@ describe('connectorTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('slack action params validation', () => {
|
||||
test('should succeed when action params include valid message', async () => {
|
||||
test('if action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
message: 'message {test}',
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: [],
|
||||
'subActionParams.channels': [],
|
||||
},
|
||||
errors: { message: [] },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -51,77 +48,6 @@ describe('slack action params validation', () => {
|
|||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
'subActionParams.channels': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should succeed when action params include valid message and channels list', async () => {
|
||||
const actionParams = {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: [],
|
||||
'subActionParams.channels': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail when action params do not includes any channels', async () => {
|
||||
const actionParams = {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: [], text: 'some text' },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: [],
|
||||
'subActionParams.channels': ['At least one selected channel is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail when channels field is missing in action params', async () => {
|
||||
const actionParams = {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { text: 'some text' },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: [],
|
||||
'subActionParams.channels': ['At least one selected channel is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail when field text doesnot exist', async () => {
|
||||
const actionParams = {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'] },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
'subActionParams.channels': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail when text is empty string', async () => {
|
||||
const actionParams = {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: '' },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
'subActionParams.channels': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,21 +11,11 @@ import type {
|
|||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants';
|
||||
import type {
|
||||
SlackActionParams,
|
||||
SlackSecrets,
|
||||
WebhookParams,
|
||||
PostMessageParams,
|
||||
} from '../../../common/slack/types';
|
||||
import { SlackActionParams, SlackSecrets } from '../types';
|
||||
|
||||
export function getConnectorType(): ConnectorTypeModel<
|
||||
unknown,
|
||||
SlackSecrets,
|
||||
WebhookParams | PostMessageParams
|
||||
> {
|
||||
export function getConnectorType(): ConnectorTypeModel<unknown, SlackSecrets, SlackActionParams> {
|
||||
return {
|
||||
id: SLACK_CONNECTOR_ID,
|
||||
id: '.slack',
|
||||
iconClass: 'logoSlack',
|
||||
selectMessage: i18n.translate('xpack.stackConnectors.components.slack.selectMessageText', {
|
||||
defaultMessage: 'Send a message to a Slack channel or user.',
|
||||
|
@ -39,45 +29,14 @@ export function getConnectorType(): ConnectorTypeModel<
|
|||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
'subActionParams.channels': new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
|
||||
if ('subAction' in actionParams) {
|
||||
if (actionParams.subAction === 'postMessage') {
|
||||
if (!actionParams.subActionParams.text) {
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
if (!actionParams.subActionParams.channels?.length) {
|
||||
errors['subActionParams.channels'].push(translations.CHANNEL_REQUIRED);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!actionParams.message) {
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./slack_connectors')),
|
||||
actionParamsFields: lazy(() => import('./slack_params')),
|
||||
resetParamsOnConnectorChange: (
|
||||
params: WebhookParams | PostMessageParams
|
||||
): WebhookParams | PostMessageParams | {} => {
|
||||
if ('message' in params) {
|
||||
return {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: {
|
||||
channels: [],
|
||||
text: params.message,
|
||||
},
|
||||
};
|
||||
} else if ('subAction' in params) {
|
||||
return {
|
||||
message: (params as PostMessageParams).subActionParams.text,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,20 +6,16 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render, fireEvent, screen } from '@testing-library/react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import SlackActionFields from './slack_connectors';
|
||||
import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils';
|
||||
import { ConnectorFormTestProvider } from '../lib/test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
|
||||
|
||||
describe('SlackActionFields renders', () => {
|
||||
const onSubmit = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('all connector fields is rendered for webhook type', async () => {
|
||||
test('all connector fields is rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
|
@ -31,174 +27,107 @@ describe('SlackActionFields renders', () => {
|
|||
isDeprecated: false,
|
||||
};
|
||||
|
||||
render(
|
||||
const wrapper = mountWithIntl(
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('webhook'));
|
||||
expect(screen.getByTestId('slackWebhookUrlInput')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('slackWebhookUrlInput')).toHaveValue('http://test.com');
|
||||
});
|
||||
|
||||
it('all connector fields is rendered for web_api type', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('slackTypeChangeButton')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('secrets.token-input')).toHaveValue('some token');
|
||||
});
|
||||
|
||||
it('should not show slack type tabs when in editing mode', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields readOnly={false} isEdit={true} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('slackTypeChangeButton')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('connector validation succeeds when connector config is valid for Web API type', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('form-test-provide-submit'));
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledTimes(1);
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
config: {
|
||||
type: 'web_api',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').first().prop('value')).toBe(
|
||||
'http://test.com'
|
||||
);
|
||||
});
|
||||
|
||||
it('connector validation succeeds when connector config is valid for Webhook type', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
await waitForComponentToUpdate();
|
||||
fireEvent.click(screen.getByTestId('webhook'));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('form-test-provide-submit'));
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledTimes(1);
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
config: {
|
||||
type: 'webhook',
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates teh web hook url field correctly', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(
|
||||
getByTestId('slackWebhookUrlInput'),
|
||||
`{selectall}{backspace}no-valid`,
|
||||
{
|
||||
delay: 10,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('validates teh web hook url field correctly', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
await waitForComponentToUpdate();
|
||||
fireEvent.click(screen.getByTestId('webhook'));
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByTestId('slackWebhookUrlInput'),
|
||||
`{selectall}{backspace}no-valid`,
|
||||
{
|
||||
delay: 10,
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,88 +5,55 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiSpacer, EuiButtonGroup } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { HiddenField } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
import { SlackWebApiActionsFields } from './slack_web_api_connectors';
|
||||
import { SlackWebhookActionFields } from './slack_webhook_connectors';
|
||||
|
||||
const slackTypeButtons = [
|
||||
{
|
||||
id: 'webhook',
|
||||
label: i18n.WEBHOOK,
|
||||
'data-test-subj': 'webhookButton',
|
||||
},
|
||||
{
|
||||
id: 'web_api',
|
||||
label: i18n.WEB_API,
|
||||
'data-test-subj': 'webApiButton',
|
||||
},
|
||||
];
|
||||
const { urlField } = fieldValidators;
|
||||
|
||||
const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: i18n.WEBHOOK_URL_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.slackAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.stackConnectors.components.slack.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Slack Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.WEBHOOK_URL_INVALID),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const SlackActionFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
isEdit,
|
||||
readOnly,
|
||||
registerPreSubmitValidator,
|
||||
}) => {
|
||||
const { setFieldValue, getFieldDefaultValue } = useFormContext();
|
||||
|
||||
const defaultSlackType = getFieldDefaultValue('config.type');
|
||||
const [selectedSlackType, setSelectedSlackType] = useState(
|
||||
getFieldDefaultValue<string>('config.type') ?? 'web_api'
|
||||
);
|
||||
|
||||
const onChange = (id: string) => {
|
||||
setSelectedSlackType(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue('config.type', selectedSlackType);
|
||||
}, [selectedSlackType, setFieldValue]);
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
{!isEdit && (
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="m"
|
||||
color="primary"
|
||||
legend={i18n.SLACK_LEGEND}
|
||||
options={slackTypeButtons}
|
||||
idSelected={selectedSlackType}
|
||||
onChange={onChange}
|
||||
data-test-subj="slackTypeChangeButton"
|
||||
/>
|
||||
)}
|
||||
<HiddenField path={'config.type'} config={{ defaultValue: defaultSlackType }} />
|
||||
<EuiSpacer size="m" />
|
||||
{/* The components size depends on slack type option we choose. Just putting a limit to form
|
||||
width would change component dehaviour during the sizing. This line make component size to
|
||||
max, so it does not change during sizing, but keep the same behaviour the designer put into
|
||||
it.
|
||||
*/}
|
||||
<div style={{ width: '100vw', height: 0 }} />
|
||||
{selectedSlackType === 'webhook' ? (
|
||||
<SlackWebhookActionFields
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
) : null}
|
||||
{selectedSlackType === 'web_api' ? (
|
||||
<>
|
||||
<SlackWebApiActionsFields
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
<UseField
|
||||
path="secrets.webhookUrl"
|
||||
config={getWebhookUrlConfig(docLinks)}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'slackWebhookUrlInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,277 +6,78 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import SlackParamsFields from './slack_params';
|
||||
import type { UseSubActionParams } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_sub_action';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
|
||||
interface Result {
|
||||
isLoading: boolean;
|
||||
response: Record<string, unknown>;
|
||||
error: null | Error;
|
||||
}
|
||||
|
||||
const triggersActionsPath = '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
const mockUseSubAction = jest.fn<Result, [UseSubActionParams<unknown>]>(
|
||||
jest.fn<Result, [UseSubActionParams<unknown>]>(() => ({
|
||||
isLoading: false,
|
||||
response: {
|
||||
channels: [
|
||||
{
|
||||
id: 'id',
|
||||
name: 'general',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
}))
|
||||
);
|
||||
|
||||
const mockToasts = { danger: jest.fn(), warning: jest.fn() };
|
||||
jest.mock(triggersActionsPath, () => {
|
||||
const original = jest.requireActual(triggersActionsPath);
|
||||
return {
|
||||
...original,
|
||||
useSubAction: (params: UseSubActionParams<unknown>) => mockUseSubAction(params),
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
notifications: { toasts: mockToasts },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SlackParamsFields renders', () => {
|
||||
test('all params fields is rendered, Webhook', () => {
|
||||
render(
|
||||
test('all params fields is rendered', () => {
|
||||
const actionParams = {
|
||||
message: 'test message',
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'webhook' } } as any}
|
||||
actionParams={{ message: 'some message' }}
|
||||
actionParams={actionParams}
|
||||
errors={{ message: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('messageTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('messageTextArea')).toHaveValue('some message');
|
||||
});
|
||||
|
||||
test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message, Webhook', () => {
|
||||
const editAction = jest.fn();
|
||||
const { rerender } = render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'webhook' } } as any}
|
||||
actionParams={{ message: 'some text' }}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={true}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('messageTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('messageTextArea')).toHaveValue('some text');
|
||||
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'webhook' } } as any}
|
||||
actionParams={{ message: 'some text' }}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="some different default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={true}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(editAction).toHaveBeenCalledWith('message', 'some different default message', 0);
|
||||
});
|
||||
|
||||
test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message, Web API', () => {
|
||||
const editAction = jest.fn();
|
||||
const { rerender } = render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={true}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text');
|
||||
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="some different default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={true}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ channels: ['general'], text: 'some different default message' },
|
||||
0
|
||||
expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual(
|
||||
'test message'
|
||||
);
|
||||
});
|
||||
|
||||
test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed, Webhook', () => {
|
||||
test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message', () => {
|
||||
const actionParams = {
|
||||
message: 'not the default message',
|
||||
};
|
||||
|
||||
const editAction = jest.fn();
|
||||
const { rerender } = render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'webhook' } } as any}
|
||||
actionParams={{ message: 'some text' }}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
const text = wrapper.find('[data-test-subj="messageTextArea"]').first().text();
|
||||
expect(text).toEqual('not the default message');
|
||||
|
||||
expect(screen.getByTestId('messageTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('messageTextArea')).toHaveValue('some text');
|
||||
wrapper.setProps({
|
||||
useDefaultMessage: true,
|
||||
defaultMessage: 'Some different default message',
|
||||
});
|
||||
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'webhook' } } as any}
|
||||
actionParams={{ message: 'some text' }}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="some different default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
expect(editAction).toHaveBeenCalledWith('message', 'Some different default message', 0);
|
||||
});
|
||||
|
||||
test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed', () => {
|
||||
const actionParams = {
|
||||
message: 'not the default message',
|
||||
};
|
||||
|
||||
const editAction = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
const text = wrapper.find('[data-test-subj="messageTextArea"]').first().text();
|
||||
expect(text).toEqual('not the default message');
|
||||
|
||||
wrapper.setProps({
|
||||
useDefaultMessage: false,
|
||||
defaultMessage: 'Some different default message',
|
||||
});
|
||||
|
||||
expect(editAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed, Web API', () => {
|
||||
const editAction = jest.fn();
|
||||
const { rerender } = render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text');
|
||||
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
defaultMessage="some different default message"
|
||||
messageVariables={[]}
|
||||
useDefaultMessage={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(editAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('all params fields is rendered, Web API, postMessage', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text');
|
||||
});
|
||||
|
||||
test('all params fields is rendered, Web API, getChannels', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<SlackParamsFields
|
||||
actionConnector={{ config: { type: 'web_api' } } as any}
|
||||
actionParams={{
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: [], text: 'some text' },
|
||||
}}
|
||||
errors={{ message: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
defaultMessage="default message"
|
||||
messageVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('slackChannelsButton')).toHaveTextContent('Channels');
|
||||
fireEvent.click(screen.getByTestId('slackChannelsButton'));
|
||||
expect(screen.getByTestId('slackChannelsSelectableList')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('slackChannelsSelectableList')).toHaveTextContent('general');
|
||||
fireEvent.click(screen.getByText('general'));
|
||||
expect(screen.getByTitle('general').getAttribute('aria-checked')).toEqual('true');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,51 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SlackWebApiParamsFields } from './slack_web_api_params';
|
||||
import { SlackWebhookParamsFields } from './slack_webhook_params';
|
||||
import { WebhookParams, PostMessageParams } from '../../../common/slack/types';
|
||||
import type { SlackActionConnector } from './types';
|
||||
import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SlackActionParams } from '../types';
|
||||
|
||||
const SlackParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<WebhookParams | PostMessageParams>
|
||||
> = (props) => {
|
||||
const { actionConnector } = props;
|
||||
const slackType = (actionConnector as unknown as SlackActionConnector)?.config?.type;
|
||||
const SlackParamsFields: React.FunctionComponent<ActionParamsProps<SlackActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
defaultMessage,
|
||||
useDefaultMessage,
|
||||
}) => {
|
||||
const { message } = actionParams;
|
||||
const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
|
||||
[boolean, string | undefined]
|
||||
>([false, defaultMessage]);
|
||||
useEffect(() => {
|
||||
if (
|
||||
useDefaultMessage ||
|
||||
!actionParams?.message ||
|
||||
(isUsingDefault &&
|
||||
actionParams?.message === defaultMessageUsed &&
|
||||
defaultMessageUsed !== defaultMessage)
|
||||
) {
|
||||
setDefaultMessageUsage([true, defaultMessage]);
|
||||
editAction('message', defaultMessage, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultMessage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!slackType || slackType === 'webhook' ? <SlackWebhookParamsFields {...props} /> : null}
|
||||
{slackType === 'web_api' ? <SlackWebApiParamsFields {...props} /> : null}
|
||||
</>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editAction}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'message'}
|
||||
inputTargetValue={message}
|
||||
label={i18n.translate('xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', {
|
||||
defaultMessage: 'Message',
|
||||
})}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,34 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActionConnectorFieldsProps,
|
||||
SecretsFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{
|
||||
id: 'token',
|
||||
label: i18n.TOKEN_LABEL,
|
||||
isPasswordField: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SlackWebApiActionsFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
isEdit,
|
||||
}) => (
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={[]}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
);
|
|
@ -1,209 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiFilterButton,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { useSubAction, useKibana } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { PostMessageParams } from '../../../common/slack/types';
|
||||
import type { GetChannelsResponse } from './types';
|
||||
|
||||
interface ChannelsStatus {
|
||||
label: string;
|
||||
checked?: 'on';
|
||||
}
|
||||
|
||||
export const SlackWebApiParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<PostMessageParams>
|
||||
> = ({
|
||||
actionConnector,
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
defaultMessage,
|
||||
useDefaultMessage,
|
||||
}) => {
|
||||
const { subAction, subActionParams } = actionParams;
|
||||
const { channels, text } = subActionParams ?? {};
|
||||
const { toasts } = useKibana().notifications;
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultMessage || !text) {
|
||||
editAction('subActionParams', { channels, text: defaultMessage }, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultMessage, useDefaultMessage]);
|
||||
|
||||
if (!subAction) {
|
||||
editAction('subAction', 'postMessage', index);
|
||||
}
|
||||
if (!subActionParams) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
channels,
|
||||
text,
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
response: { channels: channelsInfo } = {},
|
||||
isLoading: isLoadingChannels,
|
||||
error: channelsError,
|
||||
} = useSubAction<void, GetChannelsResponse>({
|
||||
connectorId: actionConnector?.id,
|
||||
subAction: 'getChannels',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channelsError) {
|
||||
toasts.danger({
|
||||
title: i18n.translate(
|
||||
'xpack.stackConnectors.slack.params.componentError.getChannelsRequestFailed',
|
||||
{
|
||||
defaultMessage: 'Failed to retrieve Slack channels list',
|
||||
}
|
||||
),
|
||||
body: channelsError.message,
|
||||
});
|
||||
}
|
||||
}, [toasts, channelsError]);
|
||||
|
||||
const slackChannels = useMemo(
|
||||
() =>
|
||||
channelsInfo
|
||||
?.filter((slackChannel) => slackChannel.is_channel)
|
||||
.map((slackChannel) => ({ label: slackChannel.name })) ?? [],
|
||||
[channelsInfo]
|
||||
);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [selectedChannels, setSelectedChannels] = useState<string[]>(channels ?? []);
|
||||
|
||||
const button = (
|
||||
<EuiFilterButton
|
||||
iconType="arrowDown"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
numFilters={selectedChannels.length}
|
||||
hasActiveFilters={selectedChannels.length > 0}
|
||||
numActiveFilters={selectedChannels.length}
|
||||
data-test-subj="slackChannelsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.stackConnectors.slack.params..showChannelsListButton"
|
||||
defaultMessage="Channels"
|
||||
/>
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
const options: ChannelsStatus[] = useMemo(
|
||||
() =>
|
||||
slackChannels.map((slackChannel) => ({
|
||||
label: slackChannel.label,
|
||||
...(selectedChannels.includes(slackChannel.label) ? { checked: 'on' } : {}),
|
||||
})),
|
||||
[slackChannels, selectedChannels]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(newOptions: EuiSelectableOption[]) => {
|
||||
const newSelectedChannels = newOptions.reduce<string[]>((result, option) => {
|
||||
if (option.checked === 'on') {
|
||||
result = [...result, option.label];
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
setSelectedChannels(newSelectedChannels);
|
||||
editAction('subActionParams', { channels: newSelectedChannels, text }, index);
|
||||
},
|
||||
[editAction, index, text]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors['subActionParams.channels']}
|
||||
isInvalid={errors['subActionParams.channels']?.length > 0 && channels !== undefined}
|
||||
>
|
||||
<EuiFilterGroup>
|
||||
<EuiPopover
|
||||
id={'id'}
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<EuiSelectable
|
||||
searchable
|
||||
data-test-subj="slackChannelsSelectableList"
|
||||
isLoading={isLoadingChannels}
|
||||
options={options}
|
||||
loadingMessage={i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.loadingMessage',
|
||||
{
|
||||
defaultMessage: 'Loading channels',
|
||||
}
|
||||
)}
|
||||
noMatchesMessage={i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.noChannelsFound',
|
||||
{
|
||||
defaultMessage: 'No channels found',
|
||||
}
|
||||
)}
|
||||
emptyMessage={i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.noChannelsAvailable',
|
||||
{
|
||||
defaultMessage: 'No channels available',
|
||||
}
|
||||
)}
|
||||
onChange={onChange}
|
||||
singleSelection={true}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
<EuiSpacer size="xs" />
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={(key: string, value: any) =>
|
||||
editAction('subActionParams', { channels, text: value }, index)
|
||||
}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty="webApi"
|
||||
inputTargetValue={text}
|
||||
label={i18n.translate('xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', {
|
||||
defaultMessage: 'Message',
|
||||
})}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,57 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const { urlField } = fieldValidators;
|
||||
|
||||
const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: i18n.WEBHOOK_URL_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.slackAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.stackConnectors.components.slack.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Slack Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.WEBHOOK_URL_INVALID),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const SlackWebhookActionFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
return (
|
||||
<UseField
|
||||
path="secrets.webhookUrl"
|
||||
config={getWebhookUrlConfig(docLinks)}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'slackWebhookUrlInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,47 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { WebhookParams } from '../../../common/slack/types';
|
||||
|
||||
export const SlackWebhookParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<WebhookParams>
|
||||
> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
defaultMessage,
|
||||
useDefaultMessage,
|
||||
}) => {
|
||||
const { message } = actionParams;
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultMessage || !message) {
|
||||
editAction('message', defaultMessage, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultMessage, useDefaultMessage]);
|
||||
|
||||
return (
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editAction}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty="message"
|
||||
inputTargetValue={message}
|
||||
label={i18n.translate('xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', {
|
||||
defaultMessage: 'Message',
|
||||
})}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -15,45 +15,15 @@ export const WEBHOOK_URL_INVALID = i18n.translate(
|
|||
);
|
||||
|
||||
export const MESSAGE_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.error.requiredSlackMessageText',
|
||||
'xpack.stackConnectors.components.slack..error.requiredSlackMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHANNEL_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.error.requiredSlackChannel',
|
||||
{
|
||||
defaultMessage: 'At least one selected channel is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.webhookUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOKEN_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.slack.tokenTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'API Token',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_TEXT = i18n.translate('xpack.stackConnectors.components.slack.urlFieldLabel', {
|
||||
defaultMessage: 'URL',
|
||||
});
|
||||
|
||||
export const SLACK_LEGEND = i18n.translate('xpack.stackConnectors.components.slack.slackLegend', {
|
||||
defaultMessage: 'Slack type',
|
||||
});
|
||||
|
||||
export const WEBHOOK = i18n.translate('xpack.stackConnectors.components.slack.webhook', {
|
||||
defaultMessage: 'Webhook',
|
||||
});
|
||||
|
||||
export const WEB_API = i18n.translate('xpack.stackConnectors.components.slack.webApi', {
|
||||
defaultMessage: 'Web API',
|
||||
});
|
||||
|
|
|
@ -1,22 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import type { SlackConfig, SlackSecrets } from '../../../common/slack/types';
|
||||
|
||||
export type SlackActionConnector = UserConfiguredActionConnector<SlackConfig, SlackSecrets>;
|
||||
|
||||
export interface GetChannelsResponse {
|
||||
ok: true;
|
||||
error?: string;
|
||||
channels?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
is_channel: boolean;
|
||||
is_archived: boolean;
|
||||
is_private: boolean;
|
||||
}>;
|
||||
}
|
|
@ -60,6 +60,10 @@ export interface ServerLogActionParams {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface SlackActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TeamsActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
@ -112,6 +116,12 @@ export type PagerDutyActionConnector = UserConfiguredActionConnector<
|
|||
PagerDutySecrets
|
||||
>;
|
||||
|
||||
export interface SlackSecrets {
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export type SlackActionConnector = UserConfiguredActionConnector<unknown, SlackSecrets>;
|
||||
|
||||
export interface WebhookConfig {
|
||||
method: string;
|
||||
url: string;
|
||||
|
|
|
@ -115,11 +115,6 @@ const XmattersActionConnectorFields: React.FunctionComponent<ActionConnectorFiel
|
|||
options={authenticationButtons}
|
||||
/>
|
||||
<HiddenField path={'config.usesBasic'} config={{ defaultValue: true }} />
|
||||
{/* The components size depends on auth option we choose. Just putting a limit to form width
|
||||
would change component dehaviour during the sizing. This line make component size to max, so
|
||||
it does not change during sizing, but keep the same behaviour the designer put into it.
|
||||
*/}
|
||||
<div style={{ width: '100vw', height: 0 }} />
|
||||
<EuiSpacer size="m" />
|
||||
{selectedAuth === XmattersAuthenticationType.URL ? (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
|
|
@ -45,8 +45,8 @@ export type { ActionParamsType as PagerDutyActionParams } from './pagerduty';
|
|||
export { ConnectorTypeId as ServerLogConnectorTypeId } from './server_log';
|
||||
export type { ActionParamsType as ServerLogActionParams } from './server_log';
|
||||
export { ServiceNowITOMConnectorTypeId } from './servicenow_itom';
|
||||
export type { SlackActionParams as SlackActionParams } from '../../common/slack/types';
|
||||
export { SLACK_CONNECTOR_ID as SlackConnectorTypeId } from '../../common/slack/constants';
|
||||
export { ConnectorTypeId as SlackConnectorTypeId } from './slack';
|
||||
export type { ActionParamsType as SlackActionParams } from './slack';
|
||||
export { ConnectorTypeId as TeamsConnectorTypeId } from './teams';
|
||||
export type { ActionParamsType as TeamsActionParams } from './teams';
|
||||
export { ConnectorTypeId as WebhookConnectorTypeId } from './webhook';
|
||||
|
@ -80,7 +80,7 @@ export function registerConnectorTypes({
|
|||
actions.registerType(getPagerDutyConnectorType());
|
||||
actions.registerType(getSwimlaneConnectorType());
|
||||
actions.registerType(getServerLogConnectorType());
|
||||
actions.registerType(getSlackConnectorType());
|
||||
actions.registerType(getSlackConnectorType({}));
|
||||
actions.registerType(getWebhookConnectorType());
|
||||
actions.registerType(getCasesWebhookConnectorType());
|
||||
actions.registerType(getXmattersConnectorType());
|
||||
|
|
|
@ -1,30 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface ResponseError {
|
||||
errorMessages: string[] | null | undefined;
|
||||
errors: { [k: string]: string } | null | undefined;
|
||||
}
|
||||
|
||||
export const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => {
|
||||
if (!errorResponse) return 'unknown: errorResponse was empty';
|
||||
|
||||
const { errorMessages, errors } = errorResponse;
|
||||
|
||||
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
|
||||
return `${errorMessages.join(', ')}`;
|
||||
}
|
||||
|
||||
if (errors == null) {
|
||||
return 'unknown: errorResponse.errors was null';
|
||||
}
|
||||
|
||||
return Object.entries(errors).reduce((errorMessage, [, value]) => {
|
||||
const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value;
|
||||
return msg;
|
||||
}, '');
|
||||
};
|
|
@ -1,101 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SlackService } from './types';
|
||||
import { api } from './api';
|
||||
|
||||
const createMock = (): jest.Mocked<SlackService> => {
|
||||
const service = {
|
||||
postMessage: jest.fn().mockImplementation(() => ({
|
||||
ok: true,
|
||||
channel: 'general',
|
||||
message: {
|
||||
text: 'a message',
|
||||
type: 'message',
|
||||
},
|
||||
})),
|
||||
getChannels: jest.fn().mockImplementation(() => [
|
||||
{
|
||||
ok: true,
|
||||
channels: [
|
||||
{
|
||||
id: 'channel_id_1',
|
||||
name: 'general',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: true,
|
||||
},
|
||||
{
|
||||
id: 'channel_id_2',
|
||||
name: 'privat',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
const slackServiceMock = {
|
||||
create: createMock,
|
||||
};
|
||||
|
||||
describe('api', () => {
|
||||
let externalService: jest.Mocked<SlackService>;
|
||||
|
||||
beforeEach(() => {
|
||||
externalService = slackServiceMock.create();
|
||||
});
|
||||
|
||||
test('getChannels', async () => {
|
||||
const res = await api.getChannels({
|
||||
externalService,
|
||||
});
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
channels: [
|
||||
{
|
||||
id: 'channel_id_1',
|
||||
is_archived: false,
|
||||
is_channel: true,
|
||||
is_private: true,
|
||||
name: 'general',
|
||||
},
|
||||
{
|
||||
id: 'channel_id_2',
|
||||
is_archived: false,
|
||||
is_channel: true,
|
||||
is_private: false,
|
||||
name: 'privat',
|
||||
},
|
||||
],
|
||||
ok: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('postMessage', async () => {
|
||||
const res = await api.postMessage({
|
||||
externalService,
|
||||
params: { channels: ['general'], text: 'a message' },
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
channel: 'general',
|
||||
message: {
|
||||
text: 'a message',
|
||||
type: 'message',
|
||||
},
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,30 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SlackService } from './types';
|
||||
import type { PostMessageSubActionParams } from '../../../common/slack/types';
|
||||
|
||||
const getChannelsHandler = async ({ externalService }: { externalService: SlackService }) => {
|
||||
const res = await externalService.getChannels();
|
||||
return res;
|
||||
};
|
||||
|
||||
const postMessageHandler = async ({
|
||||
externalService,
|
||||
params: { channels, text },
|
||||
}: {
|
||||
externalService: SlackService;
|
||||
params: PostMessageSubActionParams;
|
||||
}) => {
|
||||
const res = await externalService.postMessage({ channels, text });
|
||||
return res;
|
||||
};
|
||||
|
||||
export const api = {
|
||||
getChannels: getChannelsHandler,
|
||||
postMessage: postMessageHandler,
|
||||
};
|
|
@ -5,39 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
validateParams,
|
||||
validateSecrets,
|
||||
validateConnector,
|
||||
validateConfig,
|
||||
} from '@kbn/actions-plugin/server/lib';
|
||||
import { getConnectorType } from '.';
|
||||
Services,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import { validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import {
|
||||
getConnectorType,
|
||||
SlackConnectorType,
|
||||
SlackConnectorTypeExecutorOptions,
|
||||
ConnectorTypeId,
|
||||
} from '.';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import type { PostMessageParams, SlackConnectorType } from '../../../common/slack/types';
|
||||
import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants';
|
||||
import { SLACK_CONNECTOR_NAME } from './translations';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
jest.mock('@slack/webhook', () => {
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
IncomingWebhook: jest.fn().mockImplementation(() => {
|
||||
return { send: (message: string) => {} };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const requestMock = utils.request as jest.Mock;
|
||||
|
||||
jest.mock('@slack/webhook');
|
||||
const { IncomingWebhook } = jest.requireMock('@slack/webhook');
|
||||
|
||||
const services: Services = actionsMock.createServices();
|
||||
const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
||||
|
||||
|
@ -46,650 +38,313 @@ let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
|||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorType = getConnectorType({
|
||||
async executor(options) {
|
||||
return { status: 'ok', actionId: options.actionId };
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector registration', () => {
|
||||
test('returns connector type', () => {
|
||||
expect(connectorType.id).toEqual(SLACK_CONNECTOR_ID);
|
||||
expect(connectorType.name).toEqual(SLACK_CONNECTOR_NAME);
|
||||
expect(connectorType.id).toEqual(ConnectorTypeId);
|
||||
expect(connectorType.name).toEqual('Slack');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate params', () => {
|
||||
test('should validate and throw error when params are invalid', () => {
|
||||
describe('validateParams()', () => {
|
||||
test('should validate and pass when params is valid', () => {
|
||||
expect(
|
||||
validateParams(connectorType, { message: 'a message' }, { configurationUtilities })
|
||||
).toEqual({
|
||||
message: 'a message',
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and throw error when params is invalid', () => {
|
||||
expect(() => {
|
||||
validateParams(connectorType, {}, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined."`
|
||||
`"error validating action params: [message]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateParams(connectorType, { message: 1 }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined."`
|
||||
`"error validating action params: [message]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConnectorTypeSecrets()', () => {
|
||||
test('should validate and pass when config is valid', () => {
|
||||
validateSecrets(
|
||||
connectorType,
|
||||
{
|
||||
webhookUrl: 'https://example.com',
|
||||
},
|
||||
{ configurationUtilities }
|
||||
);
|
||||
});
|
||||
|
||||
describe('Webhook', () => {
|
||||
test('should validate and pass when params are valid', () => {
|
||||
expect(
|
||||
validateParams(connectorType, { message: 'a message' }, { configurationUtilities })
|
||||
).toEqual({ message: 'a message' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Web API', () => {
|
||||
test('should validate and pass when params are valid for post message', () => {
|
||||
expect(
|
||||
validateParams(
|
||||
connectorType,
|
||||
{ subAction: 'postMessage', subActionParams: { channels: ['general'], text: 'a text' } },
|
||||
{ configurationUtilities }
|
||||
)
|
||||
).toEqual({
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'a text' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and pass when params are valid for get channels', () => {
|
||||
expect(
|
||||
validateParams(connectorType, { subAction: 'getChannels' }, { configurationUtilities })
|
||||
).toEqual({
|
||||
subAction: 'getChannels',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate config, secrets and connector', () => {
|
||||
test('should validate and throw error when secrets is invalid', () => {
|
||||
test('should validate and throw error when config is invalid', () => {
|
||||
expect(() => {
|
||||
validateSecrets(connectorType, {}, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
"error validating action type secrets: types that failed validation:
|
||||
- [0.webhookUrl]: expected value of type [string] but got [undefined]
|
||||
- [1.token]: expected value of type [string] but got [undefined]"
|
||||
`);
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
test('should validate and pass when config is valid', () => {
|
||||
validateConfig(connectorType, { type: 'web_api' }, { configurationUtilities });
|
||||
});
|
||||
|
||||
test('should validate and pass when config is empty', () => {
|
||||
validateConfig(connectorType, {}, { configurationUtilities });
|
||||
});
|
||||
|
||||
test('should fail when config is invalid', () => {
|
||||
expect(() => {
|
||||
validateConfig(connectorType, { type: 'not_webhook' }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
"error validating action type config: [type]: types that failed validation:
|
||||
- [type.0]: expected value to equal [webhook]
|
||||
- [type.1]: expected value to equal [web_api]"
|
||||
`);
|
||||
validateSecrets(connectorType, { webhookUrl: 1 }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(connectorType, { webhookUrl: 'fee-fi-fo-fum' }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl"`
|
||||
);
|
||||
});
|
||||
|
||||
describe('Webhook', () => {
|
||||
test('should validate and pass when config and secrets are invalid together', () => {
|
||||
expect(() => {
|
||||
validateConnector(connectorType, {
|
||||
config: { type: 'webhook' },
|
||||
secrets: { token: 'fake_token' },
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type connector: Secrets of Slack type webhook should contain webhookUrl field"`
|
||||
);
|
||||
});
|
||||
test('should validate and pass when the slack webhookUrl is added to allowedHosts', () => {
|
||||
const configUtils = {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: (url: string) => {
|
||||
expect(url).toEqual('https://api.slack.com/');
|
||||
},
|
||||
};
|
||||
|
||||
test('should validate and pass when secrets is valid', () => {
|
||||
expect(
|
||||
validateSecrets(
|
||||
connectorType,
|
||||
{ webhookUrl: 'https://example.com' },
|
||||
{ configurationUtilities }
|
||||
);
|
||||
});
|
||||
|
||||
test('should validate and pass when the slack webhookUrl is added to allowedHosts', () => {
|
||||
const configUtils = {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: (url: string) => {
|
||||
expect(url).toEqual('https://api.slack.com/');
|
||||
},
|
||||
};
|
||||
actionsConfigMock.create();
|
||||
expect(
|
||||
validateSecrets(
|
||||
connectorType,
|
||||
{ webhookUrl: 'https://api.slack.com/' },
|
||||
{ configurationUtilities: configUtils }
|
||||
)
|
||||
).toEqual({
|
||||
webhookUrl: 'https://api.slack.com/',
|
||||
});
|
||||
});
|
||||
|
||||
test('config validation returns an error if the specified URL isnt added to allowedHosts', () => {
|
||||
const configUtils = {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: () => {
|
||||
throw new Error(`target hostname is not added to allowedHosts`);
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(
|
||||
connectorType,
|
||||
{ webhookUrl: 'https://api.slack.com/' },
|
||||
{ configurationUtilities: configUtils }
|
||||
);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring slack action: target hostname is not added to allowedHosts"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should validate and throw error when secrets is invalid', () => {
|
||||
expect(() => {
|
||||
validateSecrets(connectorType, { webhookUrl: 1 }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
"error validating action type secrets: types that failed validation:
|
||||
- [0.webhookUrl]: expected value of type [string] but got [number]
|
||||
- [1.token]: expected value of type [string] but got [undefined]"
|
||||
`);
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(connectorType, { webhookUrl: 'fee-fi-fo-fum' }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl"`
|
||||
);
|
||||
{ webhookUrl: 'https://api.slack.com/' },
|
||||
{ configurationUtilities: configUtils }
|
||||
)
|
||||
).toEqual({
|
||||
webhookUrl: 'https://api.slack.com/',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Web API', () => {
|
||||
test('should fail when config and secrets are invalid together', () => {
|
||||
expect(() => {
|
||||
validateConnector(connectorType, {
|
||||
config: { type: 'web_api' },
|
||||
secrets: { webhookUrl: 'https://fake_url' },
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type connector: Secrets of Slack type web_api should contain token field"`
|
||||
);
|
||||
});
|
||||
test('config validation returns an error if the specified URL isnt added to allowedHosts', () => {
|
||||
const configUtils = {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: () => {
|
||||
throw new Error(`target hostname is not added to allowedHosts`);
|
||||
},
|
||||
};
|
||||
|
||||
test('should validate and pass when secrets is valid', () => {
|
||||
expect(() => {
|
||||
validateSecrets(
|
||||
connectorType,
|
||||
{
|
||||
token: 'token',
|
||||
},
|
||||
{ configurationUtilities }
|
||||
{ webhookUrl: 'https://api.slack.com/' },
|
||||
{ configurationUtilities: configUtils }
|
||||
);
|
||||
});
|
||||
|
||||
test('should validate and throw error when secrets is invalid', () => {
|
||||
expect(() => {
|
||||
validateSecrets(connectorType, { token: 1 }, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
"error validating action type secrets: types that failed validation:
|
||||
- [0.webhookUrl]: expected value of type [string] but got [undefined]
|
||||
- [1.token]: expected value of type [string] but got [number]"
|
||||
`);
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring slack action: target hostname is not added to allowedHosts"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
describe('execute()', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
axios.create = jest.fn().mockImplementation(() => axios);
|
||||
connectorType = getConnectorType();
|
||||
});
|
||||
describe('Webhook', () => {
|
||||
test('should fail if type is webhook, but params does not include message', async () => {
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({
|
||||
getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }),
|
||||
}));
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
IncomingWebhook.mockImplementation(() => ({
|
||||
send: () => ({
|
||||
text: 'ok',
|
||||
}),
|
||||
}));
|
||||
async function mockSlackExecutor(options: SlackConnectorTypeExecutorOptions) {
|
||||
const { params } = options;
|
||||
const { message } = params;
|
||||
if (message == null) throw new Error('message property required in parameter');
|
||||
|
||||
await expect(
|
||||
connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { subAction: 'getChannels' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Slack connector parameters with type Webhook should include message field in parameters"`
|
||||
);
|
||||
});
|
||||
const failureMatch = message.match(/^failure: (.*)$/);
|
||||
if (failureMatch != null) {
|
||||
const failMessage = failureMatch[1];
|
||||
throw new Error(`slack mockExecutor failure: ${failMessage}`);
|
||||
}
|
||||
|
||||
test('should execute with success', async () => {
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({
|
||||
getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }),
|
||||
}));
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
IncomingWebhook.mockImplementation(() => ({
|
||||
send: () => ({
|
||||
text: 'ok',
|
||||
}),
|
||||
}));
|
||||
const response = await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
actionId: '.slack',
|
||||
data: { text: 'ok' },
|
||||
return {
|
||||
text: `slack mockExecutor success: ${message}`,
|
||||
actionId: '',
|
||||
status: 'ok',
|
||||
});
|
||||
});
|
||||
} as ConnectorTypeExecutorResult<void>;
|
||||
}
|
||||
|
||||
test('should return an error if test in response is not ok', async () => {
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({
|
||||
getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }),
|
||||
}));
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
IncomingWebhook.mockImplementation(() => ({
|
||||
send: () => ({
|
||||
text: 'not ok',
|
||||
}),
|
||||
}));
|
||||
const response = await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
actionId: '.slack',
|
||||
message: 'error posting slack message',
|
||||
serviceMessage: 'not ok',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return a null response from slack', async () => {
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({
|
||||
getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }),
|
||||
}));
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
IncomingWebhook.mockImplementation(() => ({
|
||||
send: jest.fn(),
|
||||
}));
|
||||
const response = await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
actionId: '.slack',
|
||||
message: 'unexpected null response from slack',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return that sending a message fails', async () => {
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({
|
||||
getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }),
|
||||
}));
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
IncomingWebhook.mockImplementation(() => ({
|
||||
send: () => {
|
||||
throw new Error('sending a message fails');
|
||||
},
|
||||
}));
|
||||
|
||||
expect(
|
||||
await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'failure: this invocation should fail' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
})
|
||||
).toEqual({
|
||||
actionId: '.slack',
|
||||
message: 'error posting slack message',
|
||||
serviceMessage: 'sending a message fails',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the mock executor with success proxy', async () => {
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType();
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: new Set(['example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType();
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will not bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: new Set(['not-example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType();
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['example.com']),
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType();
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will not proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['not-example.com']),
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType();
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'webhook' },
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
expect(connectorType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
message: '{{rogue}}',
|
||||
};
|
||||
const variables = {
|
||||
rogue: '*bold*',
|
||||
};
|
||||
const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables) as {
|
||||
message: string;
|
||||
};
|
||||
expect(params.message).toBe('`*bold*`');
|
||||
connectorType = getConnectorType({
|
||||
executor: mockSlackExecutor,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Web API', () => {
|
||||
test('should fail if type is web_api, but params does not include subAction', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
ok: true,
|
||||
message: { text: 'some text' },
|
||||
channel: 'general',
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(
|
||||
connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'web_api' },
|
||||
secrets: { token: 'some token' },
|
||||
params: {
|
||||
message: 'post message',
|
||||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Slack connector parameters with type Web API should include subAction field in parameters"`
|
||||
);
|
||||
test('calls the mock executor with success', async () => {
|
||||
const response = await connectorType.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actionId": "",
|
||||
"status": "ok",
|
||||
"text": "slack mockExecutor success: this invocation should succeed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should fail if type is web_api, but subAction is not postMessage/getChannels', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
ok: true,
|
||||
message: { text: 'some text' },
|
||||
channel: 'general',
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(
|
||||
connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'web_api' },
|
||||
secrets: { token: 'some token' },
|
||||
params: {
|
||||
subAction: 'getMessage' as 'getChannels',
|
||||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"subAction can be only postMesage or getChannels"`
|
||||
);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
expect(connectorType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
subAction: 'postMessage' as const,
|
||||
subActionParams: { text: 'some text', channels: ['general'] },
|
||||
};
|
||||
const variables = { rogue: '*bold*' };
|
||||
const params = connectorType.renderParameterTemplates!(
|
||||
paramsWithTemplates,
|
||||
variables
|
||||
) as PostMessageParams;
|
||||
expect(params.subActionParams.text).toBe('some text');
|
||||
});
|
||||
|
||||
test('should execute with success for post message', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
ok: true,
|
||||
message: { text: 'some text' },
|
||||
channel: 'general',
|
||||
},
|
||||
}));
|
||||
|
||||
const response = await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
test('calls the mock executor with failure', async () => {
|
||||
await expect(
|
||||
connectorType.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: { type: 'web_api' },
|
||||
secrets: { token: 'some token' },
|
||||
params: {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'some text' },
|
||||
},
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'failure: this invocation should fail' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"slack mockExecutor failure: this invocation should fail"`
|
||||
);
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
method: 'post',
|
||||
url: 'chat.postMessage',
|
||||
data: { channel: 'general', text: 'some text' },
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
actionId: '.slack',
|
||||
data: {
|
||||
channel: 'general',
|
||||
message: {
|
||||
text: 'some text',
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
|
||||
status: 'ok',
|
||||
});
|
||||
test('calls the mock executor with success proxy', async () => {
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
|
||||
test('should execute with success for get channels', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
ok: true,
|
||||
channels: [
|
||||
{
|
||||
id: 'id',
|
||||
name: 'general',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
const response = await connectorType.executor({
|
||||
actionId: '.slack',
|
||||
services,
|
||||
config: { type: 'web_api' },
|
||||
secrets: { token: 'some token' },
|
||||
params: {
|
||||
subAction: 'getChannels',
|
||||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
method: 'get',
|
||||
url: 'conversations.list?types=public_channel,private_channel',
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
actionId: '.slack',
|
||||
data: {
|
||||
channels: [
|
||||
{
|
||||
id: 'id',
|
||||
is_archived: false,
|
||||
is_channel: true,
|
||||
is_private: true,
|
||||
name: 'general',
|
||||
},
|
||||
],
|
||||
ok: true,
|
||||
},
|
||||
status: 'ok',
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType({});
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: new Set(['example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType({});
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will not bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: new Set(['not-example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType({});
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['example.com']),
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType({});
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will not proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configUtils = actionsConfigMock.create();
|
||||
configUtils.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxySSLSettings: {
|
||||
verificationMode: 'none',
|
||||
},
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['not-example.com']),
|
||||
});
|
||||
const connectorTypeProxy = getConnectorType({});
|
||||
await connectorTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
expect(connectorType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
message: '{{rogue}}',
|
||||
};
|
||||
const variables = {
|
||||
rogue: '*bold*',
|
||||
};
|
||||
const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables);
|
||||
expect(params.message).toBe('`*bold*`');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import HttpProxyAgent from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
|
||||
import type {
|
||||
ActionType as ConnectorType,
|
||||
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
ExecutorType,
|
||||
ValidatorServices,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
AlertingConnectorFeatureId,
|
||||
UptimeConnectorFeatureId,
|
||||
|
@ -20,83 +28,114 @@ import {
|
|||
import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer';
|
||||
import { getCustomAgents } from '@kbn/actions-plugin/server/lib/get_custom_agents';
|
||||
import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header';
|
||||
import type {
|
||||
SlackWebApiExecutorOptions,
|
||||
SlackWebhookExecutorOptions,
|
||||
WebhookParams,
|
||||
SlackExecutorOptions,
|
||||
SlackConnectorType,
|
||||
WebApiParams,
|
||||
} from '../../../common/slack/types';
|
||||
import {
|
||||
SlackSecretsSchema,
|
||||
SlackParamsSchema,
|
||||
SlackConfigSchema,
|
||||
} from '../../../common/slack/schema';
|
||||
import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants';
|
||||
import { SLACK_CONNECTOR_NAME } from './translations';
|
||||
import { api } from './api';
|
||||
import { createExternalService } from './service';
|
||||
import { validate } from './validators';
|
||||
|
||||
export function getConnectorType(): SlackConnectorType {
|
||||
export type SlackConnectorType = ConnectorType<
|
||||
{},
|
||||
ConnectorTypeSecretsType,
|
||||
ActionParamsType,
|
||||
unknown
|
||||
>;
|
||||
export type SlackConnectorTypeExecutorOptions = ConnectorTypeExecutorOptions<
|
||||
{},
|
||||
ConnectorTypeSecretsType,
|
||||
ActionParamsType
|
||||
>;
|
||||
|
||||
// secrets definition
|
||||
|
||||
export type ConnectorTypeSecretsType = TypeOf<typeof SecretsSchema>;
|
||||
|
||||
const secretsSchemaProps = {
|
||||
webhookUrl: schema.string(),
|
||||
};
|
||||
const SecretsSchema = schema.object(secretsSchemaProps);
|
||||
|
||||
// params definition
|
||||
|
||||
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
|
||||
|
||||
const ParamsSchema = schema.object({
|
||||
message: schema.string({ minLength: 1 }),
|
||||
});
|
||||
|
||||
// connector type definition
|
||||
|
||||
export const ConnectorTypeId = '.slack';
|
||||
// customizing executor is only used for tests
|
||||
export function getConnectorType({
|
||||
executor = slackExecutor,
|
||||
}: {
|
||||
executor?: ExecutorType<{}, ConnectorTypeSecretsType, ActionParamsType, unknown>;
|
||||
}): SlackConnectorType {
|
||||
return {
|
||||
id: SLACK_CONNECTOR_ID,
|
||||
id: ConnectorTypeId,
|
||||
minimumLicenseRequired: 'gold',
|
||||
name: SLACK_CONNECTOR_NAME,
|
||||
name: i18n.translate('xpack.stackConnectors.slack.title', {
|
||||
defaultMessage: 'Slack',
|
||||
}),
|
||||
supportedFeatureIds: [
|
||||
AlertingConnectorFeatureId,
|
||||
UptimeConnectorFeatureId,
|
||||
SecurityConnectorFeatureId,
|
||||
],
|
||||
validate: {
|
||||
config: {
|
||||
schema: SlackConfigSchema,
|
||||
},
|
||||
secrets: {
|
||||
schema: SlackSecretsSchema,
|
||||
customValidator: validate.secrets,
|
||||
schema: SecretsSchema,
|
||||
customValidator: validateConnectorTypeConfig,
|
||||
},
|
||||
params: {
|
||||
schema: SlackParamsSchema,
|
||||
schema: ParamsSchema,
|
||||
},
|
||||
connector: validate.connector,
|
||||
},
|
||||
renderParameterTemplates,
|
||||
executor: async (execOptions: SlackExecutorOptions) => {
|
||||
const slackType =
|
||||
!execOptions.config?.type || execOptions.config?.type === 'webhook' ? 'webhook' : 'web_api';
|
||||
validate.validateTypeParamsCombination(slackType, execOptions.params);
|
||||
|
||||
const res =
|
||||
slackType === 'webhook'
|
||||
? await slackWebhookExecutor(execOptions as SlackWebhookExecutorOptions)
|
||||
: await slackWebApiExecutor(execOptions as SlackWebApiExecutorOptions);
|
||||
return res;
|
||||
},
|
||||
executor,
|
||||
};
|
||||
}
|
||||
|
||||
const renderParameterTemplates = (
|
||||
params: WebhookParams | WebApiParams,
|
||||
function renderParameterTemplates(
|
||||
params: ActionParamsType,
|
||||
variables: Record<string, unknown>
|
||||
) => {
|
||||
if ('message' in params)
|
||||
return { message: renderMustacheString(params.message, variables, 'slack') };
|
||||
if (params.subAction === 'postMessage')
|
||||
return {
|
||||
subAction: params.subAction,
|
||||
subActionParams: {
|
||||
...params.subActionParams,
|
||||
text: renderMustacheString(params.subActionParams.text, variables, 'slack'),
|
||||
},
|
||||
};
|
||||
return params;
|
||||
};
|
||||
): ActionParamsType {
|
||||
return {
|
||||
message: renderMustacheString(params.message, variables, 'slack'),
|
||||
};
|
||||
}
|
||||
|
||||
const slackWebhookExecutor = async (
|
||||
execOptions: SlackWebhookExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> => {
|
||||
function validateConnectorTypeConfig(
|
||||
secretsObject: ConnectorTypeSecretsType,
|
||||
validatorServices: ValidatorServices
|
||||
) {
|
||||
const { configurationUtilities } = validatorServices;
|
||||
const configuredUrl = secretsObject.webhookUrl;
|
||||
try {
|
||||
new URL(configuredUrl);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.slack.configurationErrorNoHostname', {
|
||||
defaultMessage: 'error configuring slack action: unable to parse host name from webhookUrl',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configuredUrl);
|
||||
} catch (allowListError) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.slack.configurationError', {
|
||||
defaultMessage: 'error configuring slack action: {message}',
|
||||
values: {
|
||||
message: allowListError.message,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// action executor
|
||||
|
||||
async function slackExecutor(
|
||||
execOptions: SlackConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, secrets, params, configurationUtilities, logger } = execOptions;
|
||||
|
||||
let result: IncomingWebhookResult;
|
||||
|
@ -121,7 +160,6 @@ const slackWebhookExecutor = async (
|
|||
const webhook = new IncomingWebhook(webhookUrl, {
|
||||
agent,
|
||||
});
|
||||
|
||||
result = await webhook.send(message);
|
||||
} catch (err) {
|
||||
if (err.original == null || err.original.response == null) {
|
||||
|
@ -174,7 +212,7 @@ const slackWebhookExecutor = async (
|
|||
}
|
||||
|
||||
return successResult(actionId, result);
|
||||
};
|
||||
}
|
||||
|
||||
function successResult(actionId: string, data: unknown): ConnectorTypeExecutorResult<unknown> {
|
||||
return { status: 'ok', data, actionId };
|
||||
|
@ -242,47 +280,3 @@ function retryResultSeconds(
|
|||
serviceMessage: message,
|
||||
};
|
||||
}
|
||||
|
||||
const supportedSubActions = ['getChannels', 'postMessage'];
|
||||
|
||||
const slackWebApiExecutor = async (
|
||||
execOptions: SlackWebApiExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> => {
|
||||
const { actionId, params, secrets, configurationUtilities, logger } = execOptions;
|
||||
const subAction = params.subAction;
|
||||
|
||||
if (!api[subAction]) {
|
||||
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (!supportedSubActions.includes(subAction)) {
|
||||
const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
if (subAction === 'getChannels') {
|
||||
return await api.getChannels({
|
||||
externalService,
|
||||
});
|
||||
}
|
||||
|
||||
if (subAction === 'postMessage') {
|
||||
return await api.postMessage({
|
||||
externalService,
|
||||
params: params.subActionParams,
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: {}, actionId };
|
||||
};
|
||||
|
|
|
@ -1,79 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function successResult(
|
||||
actionId: string,
|
||||
data: unknown
|
||||
): ConnectorTypeExecutorResult<unknown> {
|
||||
return { status: 'ok', data, actionId };
|
||||
}
|
||||
|
||||
export function errorResult(actionId: string, message: string): ConnectorTypeExecutorResult<void> {
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
export function serviceErrorResult(
|
||||
actionId: string,
|
||||
serviceMessage?: string
|
||||
): ConnectorTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate('xpack.stackConnectors.slack.errorPostingErrorMessage', {
|
||||
defaultMessage: 'error posting slack message',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
actionId,
|
||||
serviceMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export function retryResult(actionId: string, message: string): ConnectorTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage',
|
||||
{
|
||||
defaultMessage: 'error posting a slack message, retry later',
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
retry: true,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
|
||||
export function retryResultSeconds(
|
||||
actionId: string,
|
||||
message: string,
|
||||
retryAfter: number
|
||||
): ConnectorTypeExecutorResult<void> {
|
||||
const retryEpoch = Date.now() + retryAfter * 1000;
|
||||
const retry = new Date(retryEpoch);
|
||||
const retryString = retry.toISOString();
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage',
|
||||
{
|
||||
defaultMessage: 'error posting a slack message, retry at {retryString}',
|
||||
values: {
|
||||
retryString,
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
retry,
|
||||
actionId,
|
||||
serviceMessage: message,
|
||||
};
|
||||
}
|
|
@ -1,179 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { createExternalService } from './service';
|
||||
import { request, createAxiosResponse } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { SlackService } from './types';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = request as jest.Mock;
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
const channels = [
|
||||
{
|
||||
id: 'channel_id_1',
|
||||
name: 'general',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: true,
|
||||
},
|
||||
{
|
||||
id: 'channel_id_2',
|
||||
name: 'privat',
|
||||
is_channel: true,
|
||||
is_archived: false,
|
||||
is_private: false,
|
||||
},
|
||||
];
|
||||
|
||||
const getChannelsResponse = createAxiosResponse({
|
||||
data: {
|
||||
ok: true,
|
||||
channels,
|
||||
},
|
||||
});
|
||||
|
||||
const postMessageResponse = createAxiosResponse({
|
||||
data: [
|
||||
{
|
||||
ok: true,
|
||||
channel: 'general',
|
||||
message: {
|
||||
text: 'a message',
|
||||
type: 'message',
|
||||
},
|
||||
},
|
||||
{
|
||||
ok: true,
|
||||
channel: 'privat',
|
||||
message: {
|
||||
text: 'a message',
|
||||
type: 'message',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('Slack service', () => {
|
||||
let service: SlackService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = createExternalService(
|
||||
{
|
||||
secrets: { token: 'token' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Secrets validation', () => {
|
||||
test('throws without token', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
secrets: { token: '' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[Action][Slack]: Wrong configuration."`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannels', () => {
|
||||
test('should get slack channels', async () => {
|
||||
requestMock.mockImplementation(() => getChannelsResponse);
|
||||
const res = await service.getChannels();
|
||||
expect(res).toEqual({
|
||||
actionId: '.slack',
|
||||
data: {
|
||||
ok: true,
|
||||
channels,
|
||||
},
|
||||
status: 'ok',
|
||||
});
|
||||
});
|
||||
|
||||
test('should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() => getChannelsResponse);
|
||||
|
||||
await service.getChannels();
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
method: 'get',
|
||||
url: 'conversations.list?types=public_channel,private_channel',
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw an error if request to slack fail', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('request fail');
|
||||
});
|
||||
|
||||
expect(await service.getChannels()).toEqual({
|
||||
actionId: '.slack',
|
||||
message: 'error posting slack message',
|
||||
serviceMessage: 'request fail',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('postMessage', () => {
|
||||
test('should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() => postMessageResponse);
|
||||
|
||||
await service.postMessage({ channels: ['general', 'privat'], text: 'a message' });
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
method: 'post',
|
||||
url: 'chat.postMessage',
|
||||
data: { channel: 'general', text: 'a message' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw an error if request to slack fail', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('request fail');
|
||||
});
|
||||
|
||||
expect(
|
||||
await service.postMessage({ channels: ['general', 'privat'], text: 'a message' })
|
||||
).toEqual({
|
||||
actionId: '.slack',
|
||||
message: 'error posting slack message',
|
||||
serviceMessage: 'request fail',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,170 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
|
||||
import type { SlackService, PostMessageResponse } from './types';
|
||||
import { SLACK_CONNECTOR_NAME } from './translations';
|
||||
import type { PostMessageSubActionParams } from '../../../common/slack/types';
|
||||
import { SLACK_URL } from '../../../common/slack/constants';
|
||||
import {
|
||||
retryResultSeconds,
|
||||
retryResult,
|
||||
serviceErrorResult,
|
||||
errorResult,
|
||||
successResult,
|
||||
} from './lib';
|
||||
import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants';
|
||||
import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header';
|
||||
|
||||
const buildSlackExecutorErrorResponse = ({
|
||||
slackApiError,
|
||||
logger,
|
||||
}: {
|
||||
slackApiError: {
|
||||
message: string;
|
||||
response: {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
};
|
||||
logger: Logger;
|
||||
}) => {
|
||||
if (!slackApiError.response) {
|
||||
return serviceErrorResult(SLACK_CONNECTOR_ID, slackApiError.message);
|
||||
}
|
||||
|
||||
const { status, statusText, headers } = slackApiError.response;
|
||||
|
||||
// special handling for 5xx
|
||||
if (status >= 500) {
|
||||
return retryResult(SLACK_CONNECTOR_ID, slackApiError.message);
|
||||
}
|
||||
|
||||
// special handling for rate limiting
|
||||
if (status === 429) {
|
||||
return pipe(
|
||||
getRetryAfterIntervalFromHeaders(headers),
|
||||
map((retry) => retryResultSeconds(SLACK_CONNECTOR_ID, slackApiError.message, retry)),
|
||||
getOrElse(() => retryResult(SLACK_CONNECTOR_ID, slackApiError.message))
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage',
|
||||
{
|
||||
defaultMessage: 'unexpected http response from slack: {httpStatus} {httpStatusText}',
|
||||
values: {
|
||||
httpStatus: status,
|
||||
httpStatusText: statusText,
|
||||
},
|
||||
}
|
||||
);
|
||||
logger.error(`error on ${SLACK_CONNECTOR_ID} slack action: ${errorMessage}`);
|
||||
|
||||
return errorResult(SLACK_CONNECTOR_ID, errorMessage);
|
||||
};
|
||||
|
||||
const buildSlackExecutorSuccessResponse = ({
|
||||
slackApiResponseData,
|
||||
}: {
|
||||
slackApiResponseData: PostMessageResponse;
|
||||
}) => {
|
||||
if (!slackApiResponseData) {
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage',
|
||||
{
|
||||
defaultMessage: 'unexpected null response from slack',
|
||||
}
|
||||
);
|
||||
return errorResult(SLACK_CONNECTOR_ID, errMessage);
|
||||
}
|
||||
|
||||
if (!slackApiResponseData.ok) {
|
||||
return serviceErrorResult(SLACK_CONNECTOR_ID, slackApiResponseData.error);
|
||||
}
|
||||
|
||||
return successResult(SLACK_CONNECTOR_ID, slackApiResponseData);
|
||||
};
|
||||
|
||||
export const createExternalService = (
|
||||
{ secrets }: { secrets: { token: string } },
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
): SlackService => {
|
||||
const { token } = secrets;
|
||||
|
||||
if (!token) {
|
||||
throw Error(`[Action][${SLACK_CONNECTOR_NAME}]: Wrong configuration.`);
|
||||
}
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: SLACK_URL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
});
|
||||
|
||||
const getChannels = async (): Promise<ConnectorTypeExecutorResult<unknown>> => {
|
||||
try {
|
||||
const result = await request({
|
||||
axios: axiosInstance,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
method: 'get',
|
||||
url: 'conversations.list?types=public_channel,private_channel',
|
||||
});
|
||||
|
||||
return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data });
|
||||
} catch (error) {
|
||||
return buildSlackExecutorErrorResponse({ slackApiError: error, logger });
|
||||
}
|
||||
};
|
||||
|
||||
const postMessageInOneChannel = async ({
|
||||
channel,
|
||||
text,
|
||||
}: {
|
||||
channel: string;
|
||||
text: string;
|
||||
}): Promise<ConnectorTypeExecutorResult<unknown>> => {
|
||||
try {
|
||||
const result: AxiosResponse<PostMessageResponse> = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: 'chat.postMessage',
|
||||
logger,
|
||||
data: { channel, text },
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data });
|
||||
} catch (error) {
|
||||
return buildSlackExecutorErrorResponse({ slackApiError: error, logger });
|
||||
}
|
||||
};
|
||||
|
||||
const postMessage = async ({
|
||||
channels,
|
||||
text,
|
||||
}: PostMessageSubActionParams): Promise<ConnectorTypeExecutorResult<unknown>> => {
|
||||
return await postMessageInOneChannel({ channel: channels[0], text });
|
||||
};
|
||||
|
||||
return {
|
||||
getChannels,
|
||||
postMessage,
|
||||
};
|
||||
};
|
|
@ -1,20 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SLACK_CONNECTOR_NAME = i18n.translate('xpack.stackConnectors.slack.title', {
|
||||
defaultMessage: 'Slack',
|
||||
});
|
||||
|
||||
export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
||||
i18n.translate('xpack.stackConnectors.slack.v2.configuration.apiAllowedHostsError', {
|
||||
defaultMessage: 'error configuring connector action: {message}',
|
||||
values: {
|
||||
message,
|
||||
},
|
||||
});
|
|
@ -1,26 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
|
||||
import type { PostMessageSubActionParams } from '../../../common/slack/types';
|
||||
|
||||
export interface PostMessageResponse {
|
||||
ok: boolean;
|
||||
channel?: string;
|
||||
error?: string;
|
||||
message?: {
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SlackService {
|
||||
getChannels: () => Promise<ConnectorTypeExecutorResult<unknown>>;
|
||||
postMessage: ({
|
||||
channels,
|
||||
text,
|
||||
}: PostMessageSubActionParams) => Promise<ConnectorTypeExecutorResult<unknown>>;
|
||||
}
|
|
@ -1,109 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { URL } from 'url';
|
||||
import type { SlackSecrets, SlackConfig, SlackActionParams } from '../../../common/slack/types';
|
||||
|
||||
const SECRETS_DO_NOT_MATCH_SLACK_TYPE = (slackType: 'webhook' | 'web_api', secretsField: string) =>
|
||||
i18n.translate(
|
||||
'xpack.stackConnectors.slack.configuration.apiValidateSecretsDoNotMatchSlackType',
|
||||
{
|
||||
defaultMessage: 'Secrets of Slack type {slackType} should contain {secretsField} field',
|
||||
values: {
|
||||
slackType,
|
||||
secretsField,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD = (
|
||||
slackTypeName: 'Webhook' | 'Web API',
|
||||
paramsField: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.stackConnectors.slack.configuration.slackConnectorWithTypeShouldIncludeField',
|
||||
{
|
||||
defaultMessage:
|
||||
'Slack connector parameters with type {slackTypeName} should include {paramsField} field in parameters',
|
||||
values: {
|
||||
slackTypeName,
|
||||
paramsField,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const WRONG_SUBACTION = () =>
|
||||
i18n.translate('xpack.stackConnectors.slack.configuration.slackConnectorWrongSubAction', {
|
||||
defaultMessage: 'subAction can be only postMesage or getChannels',
|
||||
});
|
||||
|
||||
export const validateSecrets = (secrets: SlackSecrets, validatorServices: ValidatorServices) => {
|
||||
if (!('webhookUrl' in secrets)) return;
|
||||
|
||||
const { configurationUtilities } = validatorServices;
|
||||
const configuredUrl = secrets.webhookUrl;
|
||||
|
||||
try {
|
||||
new URL(configuredUrl);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.slack.configurationErrorNoHostname', {
|
||||
defaultMessage: 'error configuring slack action: unable to parse host name from webhookUrl',
|
||||
})
|
||||
);
|
||||
}
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configuredUrl);
|
||||
} catch (allowListError) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.slack.configurationError', {
|
||||
defaultMessage: 'error configuring slack action: {message}',
|
||||
values: {
|
||||
message: allowListError.message,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validateConnector = (config: SlackConfig, secrets: SlackSecrets) => {
|
||||
const isWebhookType = !config?.type || config?.type === 'webhook';
|
||||
|
||||
if (isWebhookType && !('webhookUrl' in secrets)) {
|
||||
return SECRETS_DO_NOT_MATCH_SLACK_TYPE('webhook', 'webhookUrl');
|
||||
}
|
||||
if (!isWebhookType && !('token' in secrets)) {
|
||||
return SECRETS_DO_NOT_MATCH_SLACK_TYPE('web_api', 'token');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateTypeParamsCombination = (type: 'webhook' | 'web_api', params: SlackActionParams) => {
|
||||
if (type === 'webhook') {
|
||||
if (!('message' in params)) {
|
||||
throw new Error(SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD('Webhook', 'message'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (type === 'web_api') {
|
||||
if (!('subAction' in params)) {
|
||||
throw new Error(SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD('Web API', 'subAction'));
|
||||
}
|
||||
if (params.subAction !== 'postMessage' && params.subAction !== 'getChannels') {
|
||||
throw new Error(WRONG_SUBACTION());
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export const validate = {
|
||||
secrets: validateSecrets,
|
||||
connector: validateConnector,
|
||||
validateTypeParamsCombination,
|
||||
};
|
|
@ -21,6 +21,7 @@ export type {
|
|||
PagerDutyActionParams,
|
||||
ServerLogConnectorTypeId,
|
||||
ServerLogActionParams,
|
||||
SlackConnectorTypeId,
|
||||
SlackActionParams,
|
||||
WebhookConnectorTypeId,
|
||||
WebhookActionParams,
|
||||
|
|
|
@ -34116,7 +34116,7 @@
|
|||
"xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "Identificateur pour les incidents de mise à jour",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "Créez un incident dans ServiceNow SecOps.",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.title": "Incident de sécurité",
|
||||
"xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "Le message est requis.",
|
||||
"xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "Le message est requis.",
|
||||
"xpack.stackConnectors.components.slack.connectorTypeTitle": "Envoyer vers Slack",
|
||||
"xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.",
|
||||
"xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "Message",
|
||||
|
|
|
@ -34095,7 +34095,7 @@
|
|||
"xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "インシデントを更新するID",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "ServiceNow SecOpsでインシデントを作成します。",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.title": "セキュリティインシデント",
|
||||
"xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "メッセージが必要です。",
|
||||
"xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "メッセージが必要です。",
|
||||
"xpack.stackConnectors.components.slack.connectorTypeTitle": "Slack に送信",
|
||||
"xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "Web フック URL が無効です。",
|
||||
"xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "メッセージ",
|
||||
|
|
|
@ -34111,7 +34111,7 @@
|
|||
"xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "用于更新事件的标识符",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "在 ServiceNow SecOps 中创建事件。",
|
||||
"xpack.stackConnectors.components.serviceNowSIR.title": "安全事件",
|
||||
"xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "“消息”必填。",
|
||||
"xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "“消息”必填。",
|
||||
"xpack.stackConnectors.components.slack.connectorTypeTitle": "发送到 Slack",
|
||||
"xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "Webhook URL 无效。",
|
||||
"xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "消息",
|
||||
|
|
|
@ -388,23 +388,6 @@ export const ActionForm = ({
|
|||
}}
|
||||
onConnectorSelected={(id: string) => {
|
||||
setActionIdByIndex(id, index);
|
||||
const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
|
||||
if (actionTypeRegistered.resetParamsOnConnectorChange) {
|
||||
const updatedActions = actions.map((_item: RuleAction, i: number) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
..._item,
|
||||
id,
|
||||
params:
|
||||
actionTypeRegistered.resetParamsOnConnectorChange != null
|
||||
? actionTypeRegistered.resetParamsOnConnectorChange(_item.params)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
return _item;
|
||||
});
|
||||
setActions(updatedActions);
|
||||
}
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onDeleteAction={() => {
|
||||
|
|
|
@ -159,11 +159,6 @@ export const ActionTypeForm = ({
|
|||
return defaultParams;
|
||||
};
|
||||
|
||||
const handleOnConnectorSelected = (id: string) => {
|
||||
onConnectorSelected(id);
|
||||
setUseDefaultMessage(true);
|
||||
};
|
||||
|
||||
const [showMinimumThrottleWarning, showMinimumThrottleUnitWarning] = useMemo(() => {
|
||||
try {
|
||||
if (!actionThrottle) return [false, false];
|
||||
|
@ -347,7 +342,7 @@ export const ActionTypeForm = ({
|
|||
actionTypesIndex={actionTypesIndex}
|
||||
actionTypeRegistered={actionTypeRegistered}
|
||||
connectors={connectors}
|
||||
onConnectorSelected={handleOnConnectorSelected}
|
||||
onConnectorSelected={onConnectorSelected}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="xl" />
|
||||
|
|
|
@ -24,7 +24,6 @@ interface CommandType<
|
|||
| 'setRuleActionParams'
|
||||
| 'setRuleActionProperty'
|
||||
| 'setRuleActionFrequency'
|
||||
| 'clearRuleActionParams'
|
||||
> {
|
||||
type: T;
|
||||
}
|
||||
|
@ -85,10 +84,6 @@ export type RuleReducerAction =
|
|||
| {
|
||||
command: CommandType<'setRuleActionFrequency'>;
|
||||
payload: Payload<string, RuleActionParam>;
|
||||
}
|
||||
| {
|
||||
command: CommandType<'clearRuleActionParams'>;
|
||||
payload: { index: number };
|
||||
};
|
||||
|
||||
export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>;
|
||||
|
@ -101,26 +96,6 @@ export const ruleReducer = <RulePhase extends InitialRule | Rule>(
|
|||
const { rule } = state;
|
||||
|
||||
switch (action.command.type) {
|
||||
case 'clearRuleActionParams': {
|
||||
const { index } = action.payload;
|
||||
if (index === undefined || rule.actions[index] == null) {
|
||||
return state;
|
||||
} else {
|
||||
const oldAction = rule.actions.splice(index, 1)[0];
|
||||
const updatedAction = {
|
||||
...oldAction,
|
||||
params: {},
|
||||
};
|
||||
rule.actions.splice(index, 0, updatedAction);
|
||||
return {
|
||||
...state,
|
||||
rule: {
|
||||
...rule,
|
||||
actions: [...rule.actions],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'setRule': {
|
||||
const { key, value } = action.payload as Payload<'rule', RulePhase>;
|
||||
if (key === 'rule') {
|
||||
|
|
|
@ -253,7 +253,6 @@ export interface ActionTypeModel<ActionConfig = any, ActionSecrets = any, Action
|
|||
defaultRecoveredActionParams?: RecursivePartial<ActionParams>;
|
||||
customConnectorSelectItem?: CustomConnectorSelectionItem;
|
||||
isExperimental?: boolean;
|
||||
resetParamsOnConnectorChange?: (params: ActionParams) => ActionParams | {};
|
||||
}
|
||||
|
||||
export interface GenericValidationResult<T> {
|
||||
|
|
|
@ -14,50 +14,18 @@ import { getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin';
|
|||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
export default function slackTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
|
||||
const mockedSlackActionIdForWebhook = async (slackSimulatorURL: string) => {
|
||||
const { body: createdSimulatedAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack simulator',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
config: { type: 'webhook' },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return createdSimulatedAction.id;
|
||||
};
|
||||
|
||||
const mockedSlackActionIdForWebApi = async () => {
|
||||
const { body: createdSimulatedAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack simulator',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
config: { type: 'web_api' },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return createdSimulatedAction.id;
|
||||
};
|
||||
|
||||
describe('Slack', () => {
|
||||
let slackSimulatorURL = '';
|
||||
describe('slack action', () => {
|
||||
let simulatedActionId = '';
|
||||
let slackSimulatorURL: string = '';
|
||||
let slackServer: http.Server;
|
||||
let proxyServer: httpProxy | undefined;
|
||||
let proxyHaveBeenCalled = false;
|
||||
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(async () => {
|
||||
slackServer = await getSlackServer();
|
||||
const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) });
|
||||
|
@ -74,270 +42,207 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should return 200 when creating a slack action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(typeof createdAction.id).to.be('string');
|
||||
|
||||
const { body: fetchedAction } = await supertest
|
||||
.get(`/api/actions/connector/${createdAction.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedAction).to.eql({
|
||||
id: fetchedAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
config: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'http://slack.mynonexistent.com/other/stuff/in/the/path',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `error validating action type secrets: error configuring slack action: target url \"http://slack.mynonexistent.com/other/stuff/in/the/path\" is not added to the Kibana config xpack.actions.allowedHosts`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with a webhookUrl with no hostname', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'fee-fi-fo-fum',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create our slack simulator action successfully', async () => {
|
||||
const { body: createdSimulatedAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack simulator',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
simulatedActionId = createdSimulatedAction.id;
|
||||
});
|
||||
|
||||
it('should handle firing with a simulated success', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'success',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('ok');
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('should handle an empty message error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: '',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('error');
|
||||
expect(result.message).to.match(/error validating action params: \[message\]: /);
|
||||
});
|
||||
|
||||
it('should handle a 40x slack error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'invalid_payload',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/unexpected http response from slack: /);
|
||||
});
|
||||
|
||||
it('should handle a 429 slack error', async () => {
|
||||
const dateStart = new Date().getTime();
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'rate_limit',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/error posting a slack message, retry at \d\d\d\d-/);
|
||||
|
||||
const dateRetry = new Date(result.retry).getTime();
|
||||
expect(dateRetry).to.greaterThan(dateStart);
|
||||
});
|
||||
|
||||
it('should handle a 500 slack error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'status_500',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/error posting a slack message, retry later/);
|
||||
expect(result.retry).to.equal(true);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
slackServer.close();
|
||||
if (proxyServer) {
|
||||
proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Slack - Action Creation', () => {
|
||||
it('should return 200 when creating a slack action with webhook type successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'webhook' },
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'webhook' },
|
||||
});
|
||||
|
||||
expect(typeof createdAction.id).to.be('string');
|
||||
|
||||
const { body: fetchedAction } = await supertest
|
||||
.get(`/api/actions/connector/${createdAction.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedAction).to.eql({
|
||||
id: fetchedAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'webhook' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when creating a slack action with web api type successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack web api action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'web_api' },
|
||||
secrets: {
|
||||
token: 'some token',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack web api action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'web_api' },
|
||||
});
|
||||
|
||||
expect(typeof createdAction.id).to.be('string');
|
||||
|
||||
const { body: fetchedAction } = await supertest
|
||||
.get(`/api/actions/connector/${createdAction.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedAction).to.eql({
|
||||
id: fetchedAction.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
name: 'A slack web api action',
|
||||
connector_type_id: '.slack',
|
||||
config: { type: 'web_api' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: types that failed validation:\n- [0.webhookUrl]: expected value of type [string] but got [undefined]\n- [1.token]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'http://slack.mynonexistent.com/other/stuff/in/the/path',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `error validating action type secrets: error configuring slack action: target url \"http://slack.mynonexistent.com/other/stuff/in/the/path\" is not added to the Kibana config xpack.actions.allowedHosts`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a slack action with a webhookUrl with no hostname', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A slack action',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: 'fee-fi-fo-fum',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slack - Executor', () => {
|
||||
it('should handle firing with a simulated success', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL);
|
||||
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'success',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('ok');
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('should handle firing with a simulated success', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebApi();
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
subAction: 'postMessage',
|
||||
subActionParams: { channels: ['general'], text: 'really important text' },
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result).to.eql({
|
||||
status: 'error',
|
||||
message: 'error posting slack message',
|
||||
connector_id: '.slack',
|
||||
service_message: 'invalid_auth',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an empty message error', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL);
|
||||
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: '',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('error');
|
||||
expect(result.message).to.equal(
|
||||
"error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined."
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a 40x slack error', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL);
|
||||
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'invalid_payload',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/unexpected http response from slack: /);
|
||||
});
|
||||
|
||||
it('should handle a 429 slack error', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL);
|
||||
|
||||
const dateStart = new Date().getTime();
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'rate_limit',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/error posting a slack message, retry at \d\d\d\d-/);
|
||||
|
||||
const dateRetry = new Date(result.retry).getTime();
|
||||
expect(dateRetry).to.greaterThan(dateStart);
|
||||
});
|
||||
|
||||
it('should handle a 500 slack error', async () => {
|
||||
const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL);
|
||||
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
message: 'status_500',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/error posting a slack message, retry later/);
|
||||
expect(result.retry).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -100,7 +100,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('addNewActionConnectorButton-.slack');
|
||||
const slackConnectorName = generateUniqueKey();
|
||||
await testSubjects.setValue('nameInput', slackConnectorName);
|
||||
await testSubjects.click('webhookButton');
|
||||
await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com');
|
||||
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
|
||||
const createdConnectorToastTitle = await pageObjects.common.closeToast();
|
||||
|
@ -159,7 +158,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('addNewActionConnectorButton-.slack');
|
||||
const slackConnectorName = generateUniqueKey();
|
||||
await testSubjects.setValue('nameInput', slackConnectorName);
|
||||
await testSubjects.click('webhookButton');
|
||||
await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com');
|
||||
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
|
||||
const createdConnectorToastTitle = await pageObjects.common.closeToast();
|
||||
|
|
|
@ -54,7 +54,8 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex
|
|||
await testSubjects.click('.slack-card');
|
||||
|
||||
await testSubjects.setValue('nameInput', connectorName);
|
||||
await testSubjects.setValue('secrets.token-input', 'some token');
|
||||
|
||||
await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com');
|
||||
|
||||
await find.clickByCssSelector(
|
||||
'[data-test-subj="create-connector-flyout-save-btn"]:not(disabled)'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue