Create REST API to fire actions (#39463)

* Create REST API to fire actions

* Add more tests to the fire API

* Remove dead file reference

* Fix broken tests

* Fix broken test

* Update docs

* Apply PR feedback
This commit is contained in:
Mike Côté 2019-06-26 11:13:50 -04:00 committed by GitHub
parent 8680dfe478
commit e522e757ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 477 additions and 220 deletions

View file

@ -122,7 +122,7 @@ Params:
|---|---|---|
|id|The id of the action you're trying to get.|string|
#### `GET /api/action/types` List action types
#### `GET /api/action/types`: List action types
No parameters.
@ -143,6 +143,20 @@ Payload:
|references|An array of `name`, `type` and `id`. This is the same as `references` in the saved objects API. See the saved objects API documentation.<br><br>In most cases, you can leave this empty.|Array|
|version|The document version when read|string|
#### `POST /api/action/{id}/_fire`: Fire action
Params:
|Property|Description|Type|
|---|---|---|
|id|The id of the action you're trying to fire.|string|
Payload:
|Property|Description|Type|
|---|---|---|
|params|The parameters the action type requires for the execution.|object|
## Firing actions
The plugin exposes a fire function that you can use to fire actions.

View file

@ -4,9 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ActionTypeRegistry } from './action_type_registry';
type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
import { ActionTypeRegistryContract } from './types';
const createActionTypeRegistryMock = () => {
const mocked: jest.Mocked<ActionTypeRegistryContract> = {

View file

@ -57,13 +57,7 @@ Array [
`);
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
expect(call.actionType).toMatchInlineSnapshot(`
Object {
"executor": [MockFunction],
"id": "my-action-type",
"name": "My action type",
}
`);
expect(call.actionTypeRegistry).toBeTruthy();
expect(call.encryptedSavedObjectsPlugin).toBeTruthy();
expect(call.getServices).toBeTruthy();
});

View file

@ -6,19 +6,20 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { ActionType, Services } from './types';
import { TaskManager } from '../../task_manager';
import { ActionType, GetServicesFunction } from './types';
import { TaskManager, TaskRunCreatorFunction } from '../../task_manager';
import { getCreateTaskRunnerFunction } from './lib';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
interface ConstructorOptions {
getServices: (basePath: string) => Services;
taskManager: TaskManager;
getServices: GetServicesFunction;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
}
export class ActionTypeRegistry {
private readonly getServices: (basePath: string) => Services;
private readonly taskRunCreatorFunction: TaskRunCreatorFunction;
private readonly getServices: GetServicesFunction;
private readonly taskManager: TaskManager;
private readonly actionTypes: Map<string, ActionType> = new Map();
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
@ -27,6 +28,11 @@ export class ActionTypeRegistry {
this.getServices = getServices;
this.taskManager = taskManager;
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
this.taskRunCreatorFunction = getCreateTaskRunnerFunction({
actionTypeRegistry: this,
getServices: this.getServices,
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
});
}
/**
@ -55,11 +61,7 @@ export class ActionTypeRegistry {
[`actions:${actionType.id}`]: {
title: actionType.name,
type: `actions:${actionType.id}`,
createTaskRunner: getCreateTaskRunnerFunction({
actionType,
getServices: this.getServices,
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
}),
createTaskRunner: this.taskRunCreatorFunction,
},
});
}

View file

@ -16,6 +16,7 @@ import {
getRoute,
updateRoute,
listActionTypesRoute,
fireRoute,
} from './routes';
import { registerBuiltInActionTypes } from './builtin_action_types';
@ -33,7 +34,7 @@ export function init(server: Legacy.Server) {
attributesToExcludeFromAAD: new Set(['description']),
});
function getServices(basePath: string): Services {
function getServices(basePath: string, overwrites: Partial<Services> = {}): Services {
// Fake request is here to allow creating a scoped saved objects client
// and use it when security is disabled. This will be replaced when the
// future phase of API tokens is complete.
@ -45,6 +46,7 @@ export function init(server: Legacy.Server) {
log: server.log,
callCluster: callWithInternalUser,
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(fakeRequest),
...overwrites,
};
}
@ -64,6 +66,11 @@ export function init(server: Legacy.Server) {
findRoute(server);
updateRoute(server);
listActionTypesRoute(server);
fireRoute({
server,
actionTypeRegistry,
getServices,
});
const fireFn = createFireFunction({
taskManager: taskManager!,

View file

@ -0,0 +1,163 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { execute } from './execute';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
const savedObjectsClient = SavedObjectsClientMock.create();
function getServices() {
return {
savedObjectsClient,
log: jest.fn(),
callCluster: jest.fn(),
};
}
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
const actionTypeRegistry = actionTypeRegistryMock.create();
const executeParams = {
actionId: '1',
namespace: 'some-namespace',
services: getServices(),
params: {
foo: true,
},
actionTypeRegistry,
encryptedSavedObjectsPlugin,
};
beforeEach(() => jest.resetAllMocks());
test('successfully executes', async () => {
const actionType = {
id: 'test',
name: 'Test',
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
actionTypeConfig: {
bar: true,
},
actionTypeConfigSecrets: {
baz: true,
},
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await execute(executeParams);
expect(encryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith(
'action',
'1',
{ namespace: 'some-namespace' }
);
expect(actionTypeRegistry.get).toHaveBeenCalledWith('test');
expect(actionType.executor).toHaveBeenCalledWith({
services: expect.anything(),
config: {
bar: true,
baz: true,
},
params: { foo: true },
});
});
test('provides empty config when actionTypeConfig and / or actionTypeConfigSecrets is empty', async () => {
const actionType = {
id: 'test',
name: 'Test',
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await execute(executeParams);
expect(actionType.executor).toHaveBeenCalledTimes(1);
const executorCall = actionType.executor.mock.calls[0][0];
expect(executorCall.config).toMatchInlineSnapshot(`Object {}`);
});
test('throws an error when config is invalid', async () => {
const actionType = {
id: 'test',
name: 'Test',
validate: {
config: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
);
});
test('throws an error when params is invalid', async () => {
const actionType = {
id: 'test',
name: 'Test',
validate: {
params: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
`"params invalid: child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Services, ActionTypeRegistryContract } from '../types';
import { validateActionTypeConfig } from './validate_action_type_config';
import { validateActionTypeParams } from './validate_action_type_params';
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
interface ExecuteOptions {
actionId: string;
namespace: string;
services: Services;
params: Record<string, any>;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
actionTypeRegistry: ActionTypeRegistryContract;
}
export async function execute({
actionId,
namespace,
actionTypeRegistry,
services,
params,
encryptedSavedObjectsPlugin,
}: ExecuteOptions) {
// TODO: Ensure user can read the action before processing
const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', actionId, {
namespace,
});
const actionType = actionTypeRegistry.get(action.attributes.actionTypeId);
const mergedActionTypeConfig = {
...(action.attributes.actionTypeConfig || {}),
...(action.attributes.actionTypeConfigSecrets || {}),
};
const validatedConfig = validateActionTypeConfig(actionType, mergedActionTypeConfig);
const validatedParams = validateActionTypeParams(actionType, params);
await actionType.executor({
services,
config: validatedConfig,
params: validatedParams,
});
}

View file

@ -4,13 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
jest.mock('./execute', () => ({
execute: jest.fn(),
}));
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
const actionType = {
id: '1',
name: '1',
executor: jest.fn(),
};
actionTypeRegistry.get.mockReturnValue(actionType);
const getCreateTaskRunnerFunctionParams = {
getServices() {
return {
@ -19,11 +32,7 @@ const getCreateTaskRunnerFunctionParams = {
savedObjectsClient: SavedObjectsClientMock.create(),
};
},
actionType: {
id: '1',
name: '1',
executor: jest.fn(),
},
actionTypeRegistry,
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
};
@ -40,101 +49,19 @@ const taskInstanceMock = {
beforeEach(() => jest.resetAllMocks());
test('successfully executes the task', async () => {
test('executes the task by calling the executor with proper parameters', async () => {
const { execute: mockExecute } = jest.requireMock('./execute');
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'action',
references: [],
attributes: {
actionTypeConfig: { foo: true },
actionTypeConfigSecrets: { bar: true },
},
});
const runnerResult = await runner.run();
expect(runnerResult).toBeUndefined();
expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mock.calls[0])
.toMatchInlineSnapshot(`
Array [
"action",
"2",
Object {
"namespace": "test",
},
]
`);
expect(getCreateTaskRunnerFunctionParams.actionType.executor).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunctionParams.actionType.executor.mock.calls[0][0];
expect(call.config).toMatchInlineSnapshot(`
Object {
"bar": true,
"foo": true,
}
`);
expect(call.params).toMatchInlineSnapshot(`
Object {
"baz": true,
}
`);
expect(call.services).toBeTruthy();
});
test('validates params before executing the task', async () => {
const createTaskRunner = getCreateTaskRunnerFunction({
...getCreateTaskRunnerFunctionParams,
actionType: {
...getCreateTaskRunnerFunctionParams.actionType,
validate: {
params: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
},
});
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'action',
references: [],
attributes: {
actionTypeConfig: { foo: true },
actionTypeConfigSecrets: { bar: true },
},
});
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
`"params invalid: child \\"param1\\" fails because [\\"param1\\" is required]"`
);
});
test('validates config before executing the task', async () => {
const createTaskRunner = getCreateTaskRunnerFunction({
...getCreateTaskRunnerFunctionParams,
actionType: {
...getCreateTaskRunnerFunctionParams.actionType,
validate: {
config: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
},
},
});
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'action',
references: [],
attributes: {
actionTypeConfig: { foo: true },
actionTypeConfigSecrets: { bar: true },
},
});
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
);
expect(mockExecute).toHaveBeenCalledWith({
namespace: 'test',
actionId: '2',
actionTypeRegistry,
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
services: expect.anything(),
params: { baz: true },
});
});

View file

@ -4,15 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ActionType, Services } from '../types';
import { execute } from './execute';
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
import { TaskInstance } from '../../../task_manager';
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
import { validateActionTypeConfig } from './validate_action_type_config';
import { validateActionTypeParams } from './validate_action_type_params';
interface CreateTaskRunnerFunctionOptions {
getServices: (basePath: string) => Services;
actionType: ActionType;
getServices: GetServicesFunction;
actionTypeRegistry: ActionTypeRegistryContract;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
}
@ -22,29 +21,20 @@ interface TaskRunnerOptions {
export function getCreateTaskRunnerFunction({
getServices,
actionType,
actionTypeRegistry,
encryptedSavedObjectsPlugin,
}: CreateTaskRunnerFunctionOptions) {
return ({ taskInstance }: TaskRunnerOptions) => {
return {
run: async () => {
const { namespace, id, actionTypeParams } = taskInstance.params;
const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id, {
await execute({
namespace,
});
const mergedActionTypeConfig = {
...(action.attributes.actionTypeConfig || {}),
...(action.attributes.actionTypeConfigSecrets || {}),
};
const validatedActionTypeConfig = validateActionTypeConfig(
actionType,
mergedActionTypeConfig
);
const validatedActionTypeParams = validateActionTypeParams(actionType, actionTypeParams);
await actionType.executor({
actionTypeRegistry,
encryptedSavedObjectsPlugin,
actionId: id,
services: getServices(taskInstance.params.basePath),
config: validatedActionTypeConfig,
params: validatedActionTypeParams,
params: actionTypeParams,
});
},
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { execute } from './execute';
export { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
export { validateActionTypeConfig } from './validate_action_type_config';
export { validateActionTypeParams } from './validate_action_type_params';

View file

@ -8,6 +8,7 @@ import Hapi from 'hapi';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { actionsClientMock } from '../actions_client.mock';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
const defaultConfig = {
'kibana.index': '.kibana',
@ -21,6 +22,7 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
const actionsClient = actionsClientMock.create();
const actionTypeRegistry = actionTypeRegistryMock.create();
const savedObjectsClient = SavedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.create();
server.config = () => {
return {
@ -41,8 +43,21 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
},
});
server.register({
name: 'encrypted_saved_objects',
register(pluginServer: Hapi.Server) {
pluginServer.expose('isEncryptionError', encryptedSavedObjects.isEncryptionError);
pluginServer.expose('registerType', encryptedSavedObjects.registerType);
pluginServer.expose(
'getDecryptedAsInternalUser',
encryptedSavedObjects.getDecryptedAsInternalUser
);
},
});
server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient);
server.decorate('request', 'getActionsClient', () => actionsClient);
server.decorate('request', 'getBasePath', () => '/s/my-space');
return { server, savedObjectsClient, actionsClient, actionTypeRegistry };
}

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../lib/execute', () => ({
execute: jest.fn(),
}));
import { createMockServer } from './_mock_server';
import { fireRoute } from './fire';
const getServices = jest.fn();
const { server, actionTypeRegistry, savedObjectsClient } = createMockServer();
fireRoute({ server, actionTypeRegistry, getServices });
beforeEach(() => jest.resetAllMocks());
it('fires an action with proper parameters', async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { execute } = require('../lib/execute');
const request = {
method: 'POST',
url: '/api/action/1/_fire',
payload: {
params: {
foo: true,
},
},
};
getServices.mockReturnValue({
log: jest.fn(),
callCluster: jest.fn(),
savedObjectsClient: jest.fn(),
});
execute.mockResolvedValueOnce({ success: true });
const { payload, statusCode } = await server.inject(request);
expect(statusCode).toBe(204);
expect(payload).toBe('');
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"1",
]
`);
expect(execute).toHaveBeenCalledTimes(1);
const executeCall = execute.mock.calls[0][0];
expect(executeCall.params).toEqual({
foo: true,
});
expect(executeCall.actionTypeRegistry).toBeTruthy();
expect(executeCall.actionId).toBe('1');
expect(executeCall.namespace).toBeUndefined();
expect(executeCall.services).toBeTruthy();
expect(executeCall.encryptedSavedObjectsPlugin).toBeTruthy();
});

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import Hapi from 'hapi';
import { execute } from '../lib';
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
interface FireRequest extends Hapi.Request {
params: {
id: string;
};
payload: {
params: Record<string, any>;
};
}
interface FireRouteOptions {
server: Hapi.Server;
actionTypeRegistry: ActionTypeRegistryContract;
getServices: GetServicesFunction;
}
export function fireRoute({ server, actionTypeRegistry, getServices }: FireRouteOptions) {
server.route({
method: 'POST',
path: '/api/action/{id}/_fire',
options: {
response: {
emptyStatusCode: 204,
},
validate: {
options: {
abortEarly: false,
},
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
payload: Joi.object()
.keys({
params: Joi.object().required(),
})
.required(),
},
},
async handler(request: FireRequest, h: Hapi.ResponseToolkit) {
const { id } = request.params;
const { params } = request.payload;
const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request);
const savedObjectsClient = request.getSavedObjectsClient();
// Ensure user can read the action and has access to it
await savedObjectsClient.get('action', id);
await execute({
params,
actionTypeRegistry,
actionId: id,
namespace: namespace === 'default' ? undefined : namespace,
services: getServices(request.getBasePath(), { savedObjectsClient }),
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!,
});
return h.response();
},
});
}

View file

@ -10,3 +10,4 @@ export { findRoute } from './find';
export { getRoute } from './get';
export { updateRoute } from './update';
export { listActionTypesRoute } from './list_action_types';
export { fireRoute } from './fire';

View file

@ -8,6 +8,8 @@ import { SavedObjectsClientContract } from 'src/core/server';
import { ActionTypeRegistry } from './action_type_registry';
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type GetServicesFunction = (basePath: string, overwrites?: Partial<Services>) => Services;
export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
export interface SavedObjectReference {
name: string;

View file

@ -5,4 +5,4 @@
*/
export { TaskManager } from './types';
export { TaskInstance, ConcreteTaskInstance } from './task';
export { TaskInstance, ConcreteTaskInstance, TaskRunCreatorFunction } from './task';

View file

@ -5,10 +5,9 @@
*/
import expect from '@kbn/expect';
import { ES_ARCHIVER_ACTION_ID } from './constants';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
export const ES_ARCHIVER_ACTION_ID = '19cfba7c-711a-4170-8590-9a99a281e85c';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
const supertest = getService('supertest');
@ -18,11 +17,12 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
const esTestIndexName = '.kibaka-alerting-test-data';
describe('actions', () => {
describe('fire', () => {
beforeEach(() => esArchiver.load('actions/basic'));
afterEach(() => esArchiver.unload('actions/basic'));
before(async () => {
await es.indices.delete({ index: esTestIndexName, ignore: [404] });
await es.indices.create({
index: esTestIndexName,
body: {
@ -53,9 +53,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
});
after(() => es.indices.delete({ index: esTestIndexName }));
it('decrypts attributes and joins on actionTypeConfig when firing', async () => {
it('decrypts attributes and joins on actionTypeConfig when calling fire API', async () => {
await supertest
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/fire`)
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
.set('kbn-xsrf', 'foo')
.send({
params: {
@ -64,11 +64,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
message: 'Testing 123',
},
})
.expect(200)
.expect(204)
.then((resp: any) => {
expect(resp.body).to.eql({
success: true,
});
expect(resp.body).to.eql({});
});
const indexedRecord = await retry.tryForTime(5000, async () => {
const searchResult = await es.search({
@ -110,7 +108,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
});
});
it('encrypted attributes still available after update', async () => {
it('fire still works with encrypted attributes after updating an action', async () => {
const { body: updatedAction } = await supertest
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
.set('kbn-xsrf', 'foo')
@ -139,7 +137,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
},
});
await supertest
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/fire`)
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
.set('kbn-xsrf', 'foo')
.send({
params: {
@ -148,11 +146,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
message: 'Testing 123',
},
})
.expect(200)
.expect(204)
.then((resp: any) => {
expect(resp.body).to.eql({
success: true,
});
expect(resp.body).to.eql({});
});
const indexedRecord = await retry.tryForTime(5000, async () => {
const searchResult = await es.search({
@ -193,5 +189,37 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
source: 'action:test.index-record',
});
});
it(`should return 404 when action doesn't exist`, async () => {
const { body: response } = await supertest
.post('/api/action/1/_fire')
.set('kbn-xsrf', 'foo')
.send({
params: { foo: true },
})
.expect(404);
expect(response).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Saved object [action/1] not found',
});
});
it('should return 400 when payload is empty and invalid', async () => {
const { body: response } = await supertest
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
.set('kbn-xsrf', 'foo')
.send({})
.expect(400);
expect(response).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'child "params" fails because ["params" is required]',
validation: {
source: 'payload',
keys: ['params'],
},
});
});
});
}

View file

@ -17,5 +17,6 @@ export default function actionsTests({ loadTestFile }: KibanaFunctionalTestDefau
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./builtin_action_types/server_log'));
loadTestFile(require.resolve('./builtin_action_types/slack'));
loadTestFile(require.resolve('./fire'));
});
}

View file

@ -21,6 +21,7 @@ export default function alertTests({ getService }: KibanaFunctionalTestDefaultPr
const createdAlertIds: string[] = [];
before(async () => {
await destroyEsTestIndex(es);
({ name: esTestIndexName } = await setupEsTestIndex(es));
await esArchiver.load('actions/basic');
});

View file

@ -61,5 +61,5 @@ export async function setupEsTestIndex(es: any) {
}
export async function destroyEsTestIndex(es: any) {
await es.indices.delete({ index: esTestIndexName });
await es.indices.delete({ index: esTestIndexName, ignore: [404] });
}

View file

@ -19,7 +19,6 @@ export default async function ({ readConfigFile }) {
testFiles: [
require.resolve('./test_suites/task_manager'),
require.resolve('./test_suites/encrypted_saved_objects'),
require.resolve('./test_suites/actions'),
],
services: {
retry: kibanaFunctionalConfig.get('services.retry'),

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { Legacy } from 'kibana';
// eslint-disable-next-line import/no-default-export
export default function actionsPlugin(kibana: any) {
return new kibana.Plugin({
id: 'actions-test',
require: ['actions'],
init(server: Legacy.Server) {
server.route({
method: 'POST',
path: '/api/action/{id}/fire',
options: {
validate: {
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
payload: Joi.object()
.keys({
params: Joi.object(),
})
.required(),
},
},
async handler(request: any) {
await request.server.plugins.actions.fire({
id: request.params.id,
params: request.payload.params,
});
return { success: true };
},
});
},
});
}

View file

@ -1,4 +0,0 @@
{
"name": "actions-test",
"version": "kibana"
}

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
// eslint-disable-next-line import/no-default-export
export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) {
describe('actions', function actionsSuite() {
this.tags('ciGroup2');
loadTestFile(require.resolve('./actions'));
});
}