From 2269e27f0057c9e698cb1a6dfdca45d1e2b54515 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 23 Jul 2020 11:38:29 -0500 Subject: [PATCH] [ML] Add API integration tests for /filters and /calendars (#72564) Co-authored-by: Elastic Machine --- .../apis/ml/calendars/create_calendars.ts | 84 ++++++++++ .../apis/ml/calendars/delete_calendars.ts | 87 +++++++++++ .../apis/ml/calendars/get_calendars.ts | 145 ++++++++++++++++++ .../apis/ml/calendars/helpers.ts | 31 ++++ .../apis/ml/calendars/index.ts | 16 ++ .../apis/ml/calendars/update_calendars.ts | 104 +++++++++++++ .../apis/ml/filters/create_filters.ts | 127 +++++++++++++++ .../apis/ml/filters/delete_filters.ts | 95 ++++++++++++ .../apis/ml/filters/get_filters.ts | 98 ++++++++++++ .../api_integration/apis/ml/filters/index.ts | 16 ++ .../apis/ml/filters/update_filters.ts | 118 ++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 2 + x-pack/test/functional/services/ml/api.ts | 139 +++++++++++++++-- 13 files changed, 1052 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/helpers.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/create_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/delete_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/get_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/update_filters.ts diff --git a/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts new file mode 100644 index 0000000000000..f163df0109ffd --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts @@ -0,0 +1,84 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('create_calendars', function () { + const calendarId = `test_create_calendar`; + + const requestBody = { + calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: 'Test calendar', + events: [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ], + }; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should successfully create calendar by id', async () => { + await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + const results = await ml.api.getCalendar(requestBody.calendarId); + const createdCalendar = results.body.calendars[0]; + + expect(createdCalendar.calendar_id).to.eql(requestBody.calendarId); + expect(createdCalendar.description).to.eql(requestBody.description); + expect(createdCalendar.job_ids).to.eql(requestBody.job_ids); + + await ml.api.waitForEventsToExistInCalendar(calendarId, requestBody.events); + }); + + it('should not create new calendar for user without required permission', async () => { + const { body } = await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + + it('should not create new calendar for unauthorized user', async () => { + const { body } = await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts new file mode 100644 index 0000000000000..5c5d5a3c432fa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts @@ -0,0 +1,87 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_calendars', function () { + const calendarId = `test_delete_cal`; + const testCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar`, + }; + const testEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ]; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, testCalendar); + await ml.api.createCalendarEvents(calendarId, testEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should delete calendar by id', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.acknowledged).to.eql(true); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + + it('should not delete calendar for user without required permission', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForCalendarToExist(calendarId); + }); + + it('should not delete calendar for unauthorized user', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForCalendarToExist(calendarId); + }); + + it('should return 404 if invalid calendarId', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts new file mode 100644 index 0000000000000..e115986b2f092 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts @@ -0,0 +1,145 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_calendars', function () { + const testEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ]; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('get multiple calendars', function () { + const testCalendars = [1, 2, 3].map((num) => ({ + calendar_id: `test_get_cal_${num}`, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar ${num}`, + })); + + beforeEach(async () => { + for (const testCalendar of testCalendars) { + await ml.api.createCalendar(testCalendar.calendar_id, testCalendar); + await ml.api.createCalendarEvents(testCalendar.calendar_id, testEvents); + } + }); + + afterEach(async () => { + for (const testCalendar of testCalendars) { + await ml.api.deleteCalendar(testCalendar.calendar_id); + } + }); + + it('should fetch all calendars', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(testCalendars.length); + expect(body[0].events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); + }); + + it('should fetch all calendars for user with view permission', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(testCalendars.length); + expect(body[0].events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); + }); + + it('should not fetch calendars for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); + + describe('get calendar by id', function () { + const calendarId = `test_get_cal`; + const testCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar`, + }; + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, testCalendar); + await ml.api.createCalendarEvents(calendarId, testEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should fetch calendar & associated events by id', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.job_ids).to.eql(testCalendar.job_ids); + expect(body.description).to.eql(testCalendar.description); + expect(body.events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body); + }); + + it('should fetch calendar & associated events by id for user with view permission', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.job_ids).to.eql(testCalendar.job_ids); + expect(body.description).to.eql(testCalendar.description); + expect(body.events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body); + }); + + it('should not fetch calendars for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); + + it('should return 404 if invalid calendar id', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/helpers.ts b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts new file mode 100644 index 0000000000000..5d143d9b451f2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts @@ -0,0 +1,31 @@ +/* + * 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 { Calendar, CalendarEvent } from '../../../../../plugins/ml/server/models/calendar'; + +export const assertAllEventsExistInCalendar = ( + eventsToCheck: CalendarEvent[], + calendar: Calendar +): boolean => { + const updatedCalendarEvents = calendar.events as CalendarEvent[]; + let allEventsAreUpdated = true; + for (const eventToCheck of eventsToCheck) { + // if at least one of the events that we need to check is not in the updated events + // no need to continue + if ( + updatedCalendarEvents.findIndex( + (updatedEvent) => + updatedEvent.description === eventToCheck.description && + updatedEvent.start_time === eventToCheck.start_time && + updatedEvent.end_time === eventToCheck.end_time + ) < 0 + ) { + allEventsAreUpdated = false; + break; + } + } + return allEventsAreUpdated; +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/index.ts b/x-pack/test/api_integration/apis/ml/calendars/index.ts new file mode 100644 index 0000000000000..e7d824205e6cc --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('calendars', function () { + loadTestFile(require.resolve('./create_calendars')); + loadTestFile(require.resolve('./get_calendars')); + loadTestFile(require.resolve('./delete_calendars')); + loadTestFile(require.resolve('./update_calendars')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts new file mode 100644 index 0000000000000..5194370b19e66 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts @@ -0,0 +1,104 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('update_calendars', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + const calendarId = `test_update_cal`; + const originalCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1'], + description: `Test calendar`, + }; + const originalEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + ]; + + const updateCalendarRequestBody = { + calendarId, + job_ids: ['test_updated_job_1', 'test_updated_job_2'], + description: 'Updated calendar #1', + events: [ + { description: 'updated event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'updated event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ], + }; + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, originalCalendar); + await ml.api.createCalendarEvents(calendarId, originalEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should update calendar by id with new settings', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(200); + + await ml.api.waitForCalendarToExist(calendarId); + + const getCalendarResult = await ml.api.getCalendar(calendarId); + const getEventsResult = await ml.api.getCalendarEvents(calendarId); + + const updatedCalendar = getCalendarResult.body.calendars[0]; + const updatedEvents = getEventsResult.body.events; + + expect(updatedCalendar.calendar_id).to.eql(updateCalendarRequestBody.calendarId); + expect(updatedCalendar.job_ids).to.have.length(updateCalendarRequestBody.job_ids.length); + expect(updatedEvents).to.have.length(updateCalendarRequestBody.events.length); + await ml.api.waitForEventsToExistInCalendar( + updatedCalendar.calendar_id, + updateCalendarRequestBody.events + ); + }); + + it('should not allow to update calendar for user without required permission ', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + + it('should not allow to update calendar for unauthorized user', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + + it('should return error if invalid calendarId ', async () => { + await supertest + .put(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts new file mode 100644 index 0000000000000..c175d3a9a3d9c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -0,0 +1,127 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'should successfully create new filter', + user: USER.ML_POWERUSER, + requestBody: { filterId: 'safe_ip_addresses', description: '', items: ['104.236.210.185'] }, + expected: { + responseCode: 200, + responseBody: { + filter_id: 'safe_ip_addresses', + description: '', + items: ['104.236.210.185'], + }, + }, + }, + { + testTitle: 'should not create new filter for user without required permission', + user: USER.ML_VIEWER, + requestBody: { + filterId: 'safe_ip_addresses_view_only', + + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 404, + responseBody: { + error: 'Not Found', + message: 'Not Found', + }, + }, + }, + { + testTitle: 'should not create new filter for unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: { + filterId: 'safe_ip_addresses_unauthorized', + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 404, + responseBody: { + error: 'Not Found', + message: 'Not Found', + }, + }, + }, + { + testTitle: 'should return 400 bad request if invalid filterId', + user: USER.ML_POWERUSER, + requestBody: { + filterId: '@invalid_filter_id', + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 400, + responseBody: { + error: 'Bad Request', + message: 'Invalid filter_id', + }, + }, + }, + { + testTitle: 'should return 400 bad request if invalid items', + user: USER.ML_POWERUSER, + requestBody: { filterId: 'valid_filter', description: '' }, + expected: { + responseCode: 400, + responseBody: { + error: 'Bad Request', + message: 'expected value of type [array] but got [undefined]', + }, + }, + }, + ]; + + describe('create_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + for (const testData of testDataList) { + const { filterId } = testData.requestBody; + await ml.api.deleteFilter(filterId); + } + }); + + for (const testData of testDataList) { + const { testTitle, user, requestBody, expected } = testData; + it(`${testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/filters`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(expected.responseCode); + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body).to.eql(expectedResponse); + } else { + expect(body.error).to.contain(expected.responseBody.error); + expect(body.message).to.contain(expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts b/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts new file mode 100644 index 0000000000000..bb83a7f720692 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts @@ -0,0 +1,95 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const items = ['104.236.210.185']; + const validFilters = [ + { + filterId: 'filter_power', + requestBody: { description: 'Test delete filter #1', items }, + }, + { + filterId: 'filter_viewer', + requestBody: { description: 'Test delete filter (viewer)', items }, + }, + { + filterId: 'filter_unauthorized', + requestBody: { description: 'Test delete filter (unauthorized)', items }, + }, + ]; + + describe('delete_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + + it(`should delete filter by id`, async () => { + const { filterId } = validFilters[0]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.acknowledged).to.eql(true); + await ml.api.waitForFilterToNotExist(filterId); + }); + + it(`should not delete filter for user without required permission`, async () => { + const { filterId } = validFilters[1]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForFilterToExist(filterId); + }); + + it(`should not delete filter for unauthorized user`, async () => { + const { filterId } = validFilters[2]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForFilterToExist(filterId); + }); + + it(`should not allow user to delete filter if invalid filterId`, async () => { + const { body } = await supertest + .delete(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts new file mode 100644 index 0000000000000..3dd6093a9917f --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -0,0 +1,98 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const validFilters = [ + { + filterId: 'filter_1', + requestBody: { description: 'Valid filter #1', items: ['104.236.210.185'] }, + }, + { + filterId: 'filter_2', + requestBody: { description: 'Valid filter #2', items: ['104.236.210.185'] }, + }, + ]; + + describe('get_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + it(`should fetch all filters`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(validFilters.length); + }); + + it(`should not allow to retrieve filters for user without required permission`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + it(`should not allow to retrieve filters for unauthorized user`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + it(`should fetch single filter by id`, async () => { + const { filterId, requestBody } = validFilters[0]; + const { body } = await supertest + .get(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.filter_id).to.eql(filterId); + expect(body.description).to.eql(requestBody.description); + expect(body.items).to.eql(requestBody.items); + }); + + it(`should return 400 if filterId does not exist`, async () => { + const { body } = await supertest + .get(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(400); + expect(body.error).to.eql('Bad Request'); + expect(body.message).to.contain('Unable to find filter'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/index.ts b/x-pack/test/api_integration/apis/ml/filters/index.ts new file mode 100644 index 0000000000000..0c0bc4eab29ec --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('filters', function () { + loadTestFile(require.resolve('./create_filters')); + loadTestFile(require.resolve('./get_filters')); + loadTestFile(require.resolve('./delete_filters')); + loadTestFile(require.resolve('./update_filters')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts new file mode 100644 index 0000000000000..eb58d545093c4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -0,0 +1,118 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const items = ['104.236.210.185']; + const validFilters = [ + { + filterId: 'filter_power', + requestBody: { description: 'Test update filter #1', items }, + }, + { + filterId: 'filter_viewer', + requestBody: { description: 'Test update filter (viewer)', items }, + }, + { + filterId: 'filter_unauthorized', + requestBody: { description: 'Test update filter (unauthorized)', items }, + }, + ]; + + describe('update_filters', function () { + const updateFilterRequestBody = { + description: 'Updated filter #1', + removeItems: items, + addItems: ['my_new_items_1', 'my_new_items_2'], + }; + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + + it(`should update filter by id`, async () => { + const { filterId } = validFilters[0]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(200); + + expect(body.filter_id).to.eql(filterId); + expect(body.description).to.eql(updateFilterRequestBody.description); + expect(body.items).to.eql(updateFilterRequestBody.addItems); + }); + + it(`should not allow to update filter for user without required permission`, async () => { + const { filterId, requestBody: oldFilterRequest } = validFilters[1]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(404); + + // response should return not found + expect(body.error).to.eql('Not Found'); + + // and the filter should not be updated + const response = await ml.api.getFilter(filterId); + const updatedFilter = response.body.filters[0]; + expect(updatedFilter.filter_id).to.eql(filterId); + expect(updatedFilter.description).to.eql(oldFilterRequest.description); + expect(updatedFilter.items).to.eql(oldFilterRequest.items); + }); + + it(`should not allow to update filter for unauthorized user`, async () => { + const { filterId, requestBody: oldFilterRequest } = validFilters[2]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + + const response = await ml.api.getFilter(filterId); + const updatedFilter = response.body.filters[0]; + expect(updatedFilter.filter_id).to.eql(filterId); + expect(updatedFilter.description).to.eql(oldFilterRequest.description); + expect(updatedFilter.items).to.eql(oldFilterRequest.items); + }); + + it(`should return appropriate error if invalid filterId`, async () => { + const { body } = await supertest + .put(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(400); + + expect(body.message).to.contain('No filter with id'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 5c2e7a6c4b2f7..b29bc47b50394 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -58,5 +58,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./jobs')); loadTestFile(require.resolve('./results')); loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./filters')); + loadTestFile(require.resolve('./calendars')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index a48159cd7515f..9dfec3a17dec0 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,14 +5,12 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; - import { FtrProviderContext } from '../../ftr_provider_context'; - import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; - export type MlApi = ProvidedType; export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { @@ -325,19 +323,102 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, - async getCalendar(calendarId: string) { - return await esSupertest.get(`/_ml/calendars/${calendarId}`).expect(200); + async getCalendar(calendarId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/calendars/${calendarId}`).expect(expectedCode); }, - async createCalendar(calendarId: string, body = { description: '', job_ids: [] }) { + async createCalendar( + calendarId: string, + requestBody: Partial = { description: '', job_ids: [] } + ) { log.debug(`Creating calendar with id '${calendarId}'...`); - await esSupertest.put(`/_ml/calendars/${calendarId}`).send(body).expect(200); + await esSupertest.put(`/_ml/calendars/${calendarId}`).send(requestBody).expect(200); + await this.waitForCalendarToExist(calendarId); + }, + + async deleteCalendar(calendarId: string) { + log.debug(`Deleting calendar with id '${calendarId}'...`); + await esSupertest.delete(`/_ml/calendars/${calendarId}`); + + await this.waitForCalendarNotToExist(calendarId); + }, + + async waitForCalendarToExist(calendarId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${calendarId}' to exist`, 5 * 1000, async () => { + if (await this.getCalendar(calendarId, 200)) { + return true; + } else { + throw new Error(errorMsg || `expected calendar '${calendarId}' to exist`); + } + }); + }, - await retry.waitForWithTimeout(`'${calendarId}' to be created`, 30 * 1000, async () => { - if (await this.getCalendar(calendarId)) { + async waitForCalendarNotToExist(calendarId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${calendarId}' to not exist`, 5 * 1000, async () => { + if (await this.getCalendar(calendarId, 404)) { return true; } else { - throw new Error(`expected calendar '${calendarId}' to be created`); + throw new Error(errorMsg || `expected calendar '${calendarId}' to not exist`); + } + }); + }, + + async createCalendarEvents(calendarId: string, events: CalendarEvent[]) { + log.debug(`Creating events for calendar with id '${calendarId}'...`); + await esSupertest.post(`/_ml/calendars/${calendarId}/events`).send({ events }).expect(200); + await this.waitForEventsToExistInCalendar(calendarId, events); + }, + + async getCalendarEvents(calendarId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/calendars/${calendarId}/events`).expect(expectedCode); + }, + + assertAllEventsExistInCalendar: ( + eventsToCheck: CalendarEvent[], + calendar: Calendar + ): boolean => { + const updatedCalendarEvents = calendar.events as CalendarEvent[]; + let allEventsAreUpdated = true; + for (const eventToCheck of eventsToCheck) { + // if at least one of the events that we need to check is not in the updated events + // no need to continue + if ( + updatedCalendarEvents.findIndex( + (updatedEvent) => + updatedEvent.description === eventToCheck.description && + updatedEvent.start_time === eventToCheck.start_time && + updatedEvent.end_time === eventToCheck.end_time + ) < 0 + ) { + allEventsAreUpdated = false; + break; + } + } + expect(allEventsAreUpdated).to.eql( + true, + `Expected calendar ${calendar.calendar_id} to contain events ${JSON.stringify( + eventsToCheck + )}` + ); + return true; + }, + + async waitForEventsToExistInCalendar( + calendarId: string, + eventsToCheck: CalendarEvent[], + errorMsg?: string + ) { + await retry.waitForWithTimeout(`'${calendarId}' events to exist`, 5 * 1000, async () => { + // validate if calendar events have been updated with the requested events + const { body } = await this.getCalendarEvents(calendarId, 200); + + if (this.assertAllEventsExistInCalendar(eventsToCheck, body)) { + return true; + } else { + throw new Error( + errorMsg || + `expected events for calendar '${calendarId}' to have been updated correctly` + ); } }); }, @@ -515,5 +596,43 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } ); }, + + async getFilter(filterId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/filters/${filterId}`).expect(expectedCode); + }, + + async createFilter(filterId: string, requestBody: object) { + log.debug(`Creating filter with id '${filterId}'...`); + await esSupertest.put(`/_ml/filters/${filterId}`).send(requestBody).expect(200); + + await this.waitForFilterToExist(filterId, `expected filter '${filterId}' to be created`); + }, + + async deleteFilter(filterId: string) { + log.debug(`Deleting filter with id '${filterId}'...`); + await esSupertest.delete(`/_ml/filters/${filterId}`); + + await this.waitForFilterToNotExist(filterId, `expected filter '${filterId}' to be deleted`); + }, + + async waitForFilterToExist(filterId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${filterId}' to exist`, 5 * 1000, async () => { + if (await this.getFilter(filterId, 200)) { + return true; + } else { + throw new Error(errorMsg || `expected filter '${filterId}' to exist`); + } + }); + }, + + async waitForFilterToNotExist(filterId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${filterId}' to not exist`, 5 * 1000, async () => { + if (await this.getFilter(filterId, 404)) { + return true; + } else { + throw new Error(errorMsg || `expected filter '${filterId}' to not exist`); + } + }); + }, }; }