From 9cc376f7788855e6a9ed73c0a4e550da05f3882d Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Thu, 23 Jul 2020 09:51:15 -0500 Subject: [PATCH 1/6] [ML] Add API integration testing for AD annotations [ML] Remove filter APIs --- .../apis/ml/annotations/create_annotations.ts | 110 +++++++++++ .../apis/ml/annotations/delete_annotations.ts | 128 +++++++++++++ .../apis/ml/annotations/get_annotations.ts | 167 +++++++++++++++++ .../apis/ml/annotations/index.ts | 16 ++ .../apis/ml/annotations/update_annotations.ts | 177 ++++++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 17 +- x-pack/test/functional/services/ml/api.ts | 81 +++++++- 7 files changed, 685 insertions(+), 11 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts new file mode 100644 index 0000000000000..8e11270d24cad --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -0,0 +1,110 @@ +/* + * 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 { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `job_annotation_${Date.now()}`; + const testJobConfig = { + job_id: jobId, + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + }; + const annotationRequestBody = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; + + describe('create_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAnomalyDetectionJob(testJobConfig); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should successfully create annotations for anomaly job', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + const annotationId = body._id; + + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation.annotation).to.eql(annotationRequestBody.annotation); + expect(fetchedAnnotation.job_id).to.eql(annotationRequestBody.job_id); + expect(fetchedAnnotation.event).to.eql(annotationRequestBody.event); + expect(fetchedAnnotation.user).to.eql(annotationRequestBody.user); + expect(fetchedAnnotation.create_username).to.eql(USER.ML_POWERUSER); + }); + + it('should successfully create annotation for user without required permission', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody); + + const annotationId = body._id; + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation.annotation).to.eql(annotationRequestBody.annotation); + expect(fetchedAnnotation.job_id).to.eql(annotationRequestBody.job_id); + expect(fetchedAnnotation.event).to.eql(annotationRequestBody.event); + expect(fetchedAnnotation.user).to.eql(annotationRequestBody.user); + expect(fetchedAnnotation.create_username).to.eql(USER.ML_VIEWER); + }); + + it('should not allow to create annotation for unauthorized user', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts new file mode 100644 index 0000000000000..5434b4804c8d8 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -0,0 +1,128 @@ +/* + * 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 { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [1, 2, 3].map((num) => ({ + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + })); + const jobIds = testSetupJobConfigs.map((j) => j.job_id); + + const createAnnotationRequestBody = (jobId) => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; + }; + + const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) + ); + + describe('delete_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should delete annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should delete annotation by id for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should not delete annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .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'); + + await ml.api.waitForAnnotationToExist(annotationIdToDelete); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts new file mode 100644 index 0000000000000..77f5a528e5a94 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -0,0 +1,167 @@ +/* + * 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 _ from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [1, 2, 3].map((num) => ({ + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + })); + const jobIds = testSetupJobConfigs.map((j) => j.job_id); + + const createAnnotationRequestBody = (jobId) => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; + }; + + const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) + ); + + describe('get_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all annotations for jobId', async () => { + const requestBody = { + jobIds: [jobIds[0]], + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + [jobIds[0]].forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for multiple jobs', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for user with viewer permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should not allow to fetch annotation for user with viewer permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .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'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts new file mode 100644 index 0000000000000..7d73ee43d4d99 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/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('annotations', function () { + loadTestFile(require.resolve('./create_annotations')); + loadTestFile(require.resolve('./get_annotations')); + loadTestFile(require.resolve('./delete_annotations')); + loadTestFile(require.resolve('./update_annotations')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts new file mode 100644 index 0000000000000..f27350d26e777 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -0,0 +1,177 @@ +/* + * 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 { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [1, 2, 3].map((num) => ({ + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'sum', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + })); + const jobIds = testSetupJobConfigs.map((j) => j.job_id); + + const createAnnotationRequestBody = (jobId) => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; + }; + + const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) + ); + + describe('update_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should correctly update annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation #0', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + _id: originalAnnotation._id, + }; + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation.annotation).to.eql(annotationUpdateRequestBody.annotation); + expect(updatedAnnotation.detector_index).to.eql(annotationUpdateRequestBody.detector_index); + expect(updatedAnnotation.event).to.eql(annotationUpdateRequestBody.event); + expect(updatedAnnotation.partition_field_value).to.eql( + annotationUpdateRequestBody.partition_field_value + ); + }); + + it('should correctly update annotation for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation #1', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation.annotation).to.eql(annotationUpdateRequestBody.annotation); + expect(updatedAnnotation.detector_index).to.eql(annotationUpdateRequestBody.detector_index); + expect(updatedAnnotation.event).to.eql(annotationUpdateRequestBody.event); + expect(updatedAnnotation.partition_field_value).to.eql( + originalAnnotation.partition_field_value + ); + }); + + it('should not update annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + _id: originalAnnotation._id, + }; + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation).to.eql(originalAnnotation._source); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 5c2e7a6c4b2f7..bc3be2ad87442 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -50,13 +50,14 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./modules')); - loadTestFile(require.resolve('./anomaly_detectors')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./fields_service')); - loadTestFile(require.resolve('./job_validation')); - loadTestFile(require.resolve('./jobs')); - loadTestFile(require.resolve('./results')); - loadTestFile(require.resolve('./data_frame_analytics')); + // loadTestFile(require.resolve('./modules')); + // loadTestFile(require.resolve('./anomaly_detectors')); + // loadTestFile(require.resolve('./data_visualizer')); + // loadTestFile(require.resolve('./fields_service')); + // loadTestFile(require.resolve('./job_validation')); + // loadTestFile(require.resolve('./jobs')); + // loadTestFile(require.resolve('./results')); + // loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index a48159cd7515f..02426d9a05015 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,15 +5,18 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { Annotation } from '../../../../plugins/ml/common/types/annotations'; 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; +import { + ML_ANNOTATIONS_INDEX_ALIAS_READ, + ML_ANNOTATIONS_INDEX_ALIAS_WRITE, +} from '../../../../plugins/ml/common/constants/index_patterns'; +import { IndexParams } from '../../../../plugins/ml/server/models/annotation_service/annotation'; export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -515,5 +518,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } ); }, + + async getAnnotations(jobId: string) { + log.debug(`Fetching annotations for job '${jobId}'...`); + + const results = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + job_id: jobId, + }, + }, + }, + }); + expect(results).to.not.be(undefined); + expect(results).to.have.property('hits'); + return results.hits.hits; + }, + + async getAnnotationById(annotationId: string) { + log.debug(`Fetching annotation '${annotationId}'...`); + + const result = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + _id: annotationId, + }, + }, + }, + }); + if (result.hits.total.value === 1) { + return result?.hits?.hits[0]?._source; + } + return undefined; + }, + + async indexAnnotation(annotationRequestBody: Annotation) { + log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); + const params: IndexParams = { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + body: annotationRequestBody, + refresh: 'wait_for', + }; + const results = await es.index(params); + + await this.waitForAnnotationToExist(results._id); + return results; + }, + + async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) !== undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`); + } + }); + }, + + async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) === undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`); + } + }); + }, }; } From de72d596e3849193f4262c6d446da25aa2b96a29 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Thu, 23 Jul 2020 10:04:43 -0500 Subject: [PATCH 2/6] [ML] Uncomment other test files --- x-pack/test/api_integration/apis/ml/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index bc3be2ad87442..1731baefd2242 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -50,14 +50,14 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - // loadTestFile(require.resolve('./modules')); - // loadTestFile(require.resolve('./anomaly_detectors')); - // loadTestFile(require.resolve('./data_visualizer')); - // loadTestFile(require.resolve('./fields_service')); - // loadTestFile(require.resolve('./job_validation')); - // loadTestFile(require.resolve('./jobs')); - // loadTestFile(require.resolve('./results')); - // loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./modules')); + loadTestFile(require.resolve('./anomaly_detectors')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./fields_service')); + loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./jobs')); + loadTestFile(require.resolve('./results')); + loadTestFile(require.resolve('./data_frame_analytics')); loadTestFile(require.resolve('./annotations')); }); } From a985f07ab99ae1d8b66c8ea827a9d0c8590cada0 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 28 Jul 2020 10:55:15 -0500 Subject: [PATCH 3/6] [ML] Fix type issues --- .../apis/ml/annotations/create_annotations.ts | 20 +++++++-------- .../apis/ml/annotations/delete_annotations.ts | 2 +- .../apis/ml/annotations/get_annotations.ts | 2 +- .../apis/ml/annotations/update_annotations.ts | 20 ++++++--------- x-pack/test/functional/services/ml/api.ts | 25 +++++++++++++------ 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts index 8e11270d24cad..6aaebf8de6972 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -72,11 +72,11 @@ export default ({ getService }: FtrProviderContext) => { const annotationId = body._id; const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); - expect(fetchedAnnotation.annotation).to.eql(annotationRequestBody.annotation); - expect(fetchedAnnotation.job_id).to.eql(annotationRequestBody.job_id); - expect(fetchedAnnotation.event).to.eql(annotationRequestBody.event); - expect(fetchedAnnotation.user).to.eql(annotationRequestBody.user); - expect(fetchedAnnotation.create_username).to.eql(USER.ML_POWERUSER); + expect(fetchedAnnotation).to.not.be(undefined); + expect(fetchedAnnotation?.annotation).to.eql(annotationRequestBody.annotation); + expect(fetchedAnnotation?.job_id).to.eql(annotationRequestBody.job_id); + expect(fetchedAnnotation?.event).to.eql(annotationRequestBody.event); + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); }); it('should successfully create annotation for user without required permission', async () => { @@ -88,11 +88,11 @@ export default ({ getService }: FtrProviderContext) => { const annotationId = body._id; const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); - expect(fetchedAnnotation.annotation).to.eql(annotationRequestBody.annotation); - expect(fetchedAnnotation.job_id).to.eql(annotationRequestBody.job_id); - expect(fetchedAnnotation.event).to.eql(annotationRequestBody.event); - expect(fetchedAnnotation.user).to.eql(annotationRequestBody.user); - expect(fetchedAnnotation.create_username).to.eql(USER.ML_VIEWER); + expect(fetchedAnnotation).to.not.be(undefined); + expect(fetchedAnnotation?.annotation).to.eql(annotationRequestBody.annotation); + expect(fetchedAnnotation?.job_id).to.eql(annotationRequestBody.job_id); + expect(fetchedAnnotation?.event).to.eql(annotationRequestBody.event); + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); }); it('should not allow to create annotation for unauthorized user', async () => { diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts index 5434b4804c8d8..4bf9a3fd3d831 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext) => { })); const jobIds = testSetupJobConfigs.map((j) => j.job_id); - const createAnnotationRequestBody = (jobId) => { + const createAnnotationRequestBody = (jobId: string) => { return { timestamp: Date.now(), end_timestamp: Date.now(), diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts index 77f5a528e5a94..885d49006e52c 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext) => { })); const jobIds = testSetupJobConfigs.map((j) => j.job_id); - const createAnnotationRequestBody = (jobId) => { + const createAnnotationRequestBody = (jobId: string) => { return { timestamp: Date.now(), end_timestamp: Date.now(), diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts index f27350d26e777..1011bd6a4b42d 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -39,7 +39,7 @@ export default ({ getService }: FtrProviderContext) => { })); const jobIds = testSetupJobConfigs.map((j) => j.job_id); - const createAnnotationRequestBody = (jobId) => { + const createAnnotationRequestBody = (jobId: string) => { return { timestamp: Date.now(), end_timestamp: Date.now(), @@ -103,12 +103,9 @@ export default ({ getService }: FtrProviderContext) => { expect(body.result).to.eql('updated'); const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); - expect(updatedAnnotation.annotation).to.eql(annotationUpdateRequestBody.annotation); - expect(updatedAnnotation.detector_index).to.eql(annotationUpdateRequestBody.detector_index); - expect(updatedAnnotation.event).to.eql(annotationUpdateRequestBody.event); - expect(updatedAnnotation.partition_field_value).to.eql( - annotationUpdateRequestBody.partition_field_value - ); + expect(updatedAnnotation?.annotation).to.eql(annotationUpdateRequestBody.annotation); + expect(updatedAnnotation?.detector_index).to.eql(annotationUpdateRequestBody.detector_index); + expect(updatedAnnotation?.event).to.eql(annotationUpdateRequestBody.event); }); it('should correctly update annotation for user with viewer permission', async () => { @@ -137,12 +134,9 @@ export default ({ getService }: FtrProviderContext) => { expect(body.result).to.eql('updated'); const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); - expect(updatedAnnotation.annotation).to.eql(annotationUpdateRequestBody.annotation); - expect(updatedAnnotation.detector_index).to.eql(annotationUpdateRequestBody.detector_index); - expect(updatedAnnotation.event).to.eql(annotationUpdateRequestBody.event); - expect(updatedAnnotation.partition_field_value).to.eql( - originalAnnotation.partition_field_value - ); + expect(updatedAnnotation?.annotation).to.eql(annotationUpdateRequestBody.annotation); + expect(updatedAnnotation?.detector_index).to.eql(annotationUpdateRequestBody.detector_index); + expect(updatedAnnotation?.event).to.eql(annotationUpdateRequestBody.event); }); it('should not update annotation for unauthorized user', async () => { diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 320d2bb99bbce..7874a215dcde3 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { IndexDocumentParams } from 'elasticsearch'; import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; @@ -17,7 +18,16 @@ import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, } from '../../../../plugins/ml/common/constants/index_patterns'; -import { IndexParams } from '../../../../plugins/ml/server/models/annotation_service/annotation'; + +interface EsIndexResult { + _index: string; + _id: string; + _version: number; + result: string; + _shards: any; + _seq_no: number; + _primary_term: number; +} export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -644,7 +654,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async getAnnotations(jobId: string) { log.debug(`Fetching annotations for job '${jobId}'...`); - const results = await es.search({ + const results = await es.search({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, body: { size: 1, @@ -660,7 +670,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return results.hits.hits; }, - async getAnnotationById(annotationId: string) { + async getAnnotationById(annotationId: string): Promise { log.debug(`Fetching annotation '${annotationId}'...`); const result = await es.search({ @@ -674,21 +684,22 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, }, }); + // @ts-ignore due to outdated type for hits.total if (result.hits.total.value === 1) { - return result?.hits?.hits[0]?._source; + return result?.hits?.hits[0]?._source as Annotation; } return undefined; }, async indexAnnotation(annotationRequestBody: Annotation) { log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); - const params: IndexParams = { + // @ts-ignore due to outdated type for IndexDocumentParams.type + const params: IndexDocumentParams = { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, body: annotationRequestBody, refresh: 'wait_for', }; - const results = await es.index(params); - + const results: EsIndexResult = await es.index(params); await this.waitForAnnotationToExist(results._id); return results; }, From f45a447e78d38637d6649fcb92e6d3235b13c906 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 29 Jul 2020 12:40:55 -0500 Subject: [PATCH 4/6] [ML] Refactor job configs for annotations & add more checks --- .../apis/ml/annotations/common_jobs.ts | 57 ++++++++ .../apis/ml/annotations/create_annotations.ts | 50 +++---- .../apis/ml/annotations/delete_annotations.ts | 39 +----- .../apis/ml/annotations/get_annotations.ts | 53 ++------ .../apis/ml/annotations/update_annotations.ts | 128 ++++++++++-------- x-pack/test/functional/services/ml/api.ts | 1 - 6 files changed, 158 insertions(+), 170 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts new file mode 100644 index 0000000000000..6a52cfa57dec3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -0,0 +1,57 @@ +/* + * 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 { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; + +export const commonJobConfig = { + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, +}; + +export const createJobConfig = (jobId: string) => { + return { ...commonJobConfig, job_id: jobId }; +}; + +export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ + ...commonJobConfig, + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, +})); +export const jobIds = testSetupJobConfigs.map((j) => j.job_id); + +const createAnnotationRequestBody = (jobId: string) => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; +}; + +export const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) +); diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts index 6aaebf8de6972..bb9348378c54f 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -10,7 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; import { USER } from '../../../../functional/services/ml/security_common'; import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; - +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { createJobConfig } from './common_jobs'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); @@ -18,28 +19,8 @@ export default ({ getService }: FtrProviderContext) => { const ml = getService('ml'); const jobId = `job_annotation_${Date.now()}`; - const testJobConfig = { - job_id: jobId, - description: 'test_job_annotation', - groups: ['farequote', 'automated', 'single-metric'], - analysis_config: { - bucket_span: '15m', - influencers: [], - detectors: [ - { - function: 'mean', - field_name: 'responsetime', - }, - { - function: 'min', - field_name: 'responsetime', - }, - ], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '10mb' }, - }; - const annotationRequestBody = { + const testJobConfig = createJobConfig(jobId); + const annotationRequestBody: Partial = { timestamp: Date.now(), end_timestamp: Date.now(), annotation: 'Test annotation', @@ -72,10 +53,15 @@ export default ({ getService }: FtrProviderContext) => { const annotationId = body._id; const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation).to.not.be(undefined); - expect(fetchedAnnotation?.annotation).to.eql(annotationRequestBody.annotation); - expect(fetchedAnnotation?.job_id).to.eql(annotationRequestBody.job_id); - expect(fetchedAnnotation?.event).to.eql(annotationRequestBody.event); + + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); }); @@ -84,14 +70,18 @@ export default ({ getService }: FtrProviderContext) => { .put('/api/ml/annotations/index') .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) .set(COMMON_REQUEST_HEADERS) - .send(annotationRequestBody); + .send(annotationRequestBody) + .expect(200); const annotationId = body._id; const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); expect(fetchedAnnotation).to.not.be(undefined); - expect(fetchedAnnotation?.annotation).to.eql(annotationRequestBody.annotation); - expect(fetchedAnnotation?.job_id).to.eql(annotationRequestBody.job_id); - expect(fetchedAnnotation?.event).to.eql(annotationRequestBody.event); + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); }); diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts index 4bf9a3fd3d831..4fbb26e9b5a3e 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; import { USER } from '../../../../functional/services/ml/security_common'; -import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -16,43 +16,6 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); - const testSetupJobConfigs = [1, 2, 3].map((num) => ({ - job_id: `job_annotation_${num}_${Date.now()}`, - description: `Test annotation ${num}`, - groups: ['farequote', 'automated', 'single-metric'], - analysis_config: { - bucket_span: '15m', - influencers: [], - detectors: [ - { - function: 'mean', - field_name: 'responsetime', - }, - ], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '10mb' }, - })); - const jobIds = testSetupJobConfigs.map((j) => j.job_id); - - const createAnnotationRequestBody = (jobId: string) => { - return { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Test annotation', - job_id: jobId, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'user', - detector_index: 1, - partition_field_name: 'airline', - partition_field_value: 'AAL', - }; - }; - - const testSetupAnnotations = testSetupJobConfigs.map((job) => - createAnnotationRequestBody(job.job_id) - ); - describe('delete_annotations', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts index 885d49006e52c..710473eed6901 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -5,11 +5,11 @@ */ import expect from '@kbn/expect'; -import _ from 'lodash'; +import { omit } from 'lodash'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; import { USER } from '../../../../functional/services/ml/security_common'; -import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -17,43 +17,6 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); - const testSetupJobConfigs = [1, 2, 3].map((num) => ({ - job_id: `job_annotation_${num}_${Date.now()}`, - description: `Test annotation ${num}`, - groups: ['farequote', 'automated', 'single-metric'], - analysis_config: { - bucket_span: '15m', - influencers: [], - detectors: [ - { - function: 'mean', - field_name: 'responsetime', - }, - ], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '10mb' }, - })); - const jobIds = testSetupJobConfigs.map((j) => j.job_id); - - const createAnnotationRequestBody = (jobId: string) => { - return { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Test annotation', - job_id: jobId, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'user', - detector_index: 1, - partition_field_name: 'airline', - partition_field_value: 'AAL', - }; - }; - - const testSetupAnnotations = testSetupJobConfigs.map((job) => - createAnnotationRequestBody(job.job_id) - ); - describe('get_annotations', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); @@ -92,14 +55,14 @@ export default ({ getService }: FtrProviderContext) => { expect(body.annotations).to.have.property(jobId); expect(body.annotations[jobId]).to.have.length(1); - const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); }); }); it('should fetch all annotations for multiple jobs', async () => { const requestBody = { - jobIds: testSetupJobConfigs.map((j) => j.job_id), + jobIds, earliestMs: 1454804100000, latestMs: Date.now(), maxAnnotations: 500, @@ -117,12 +80,12 @@ export default ({ getService }: FtrProviderContext) => { expect(body.annotations).to.have.property(jobId); expect(body.annotations[jobId]).to.have.length(1); - const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); }); }); - it('should fetch all annotations for user with viewer permissions', async () => { + it('should fetch all annotations for user with ML read permissions', async () => { const requestBody = { jobIds: testSetupJobConfigs.map((j) => j.job_id), earliestMs: 1454804100000, @@ -141,12 +104,12 @@ export default ({ getService }: FtrProviderContext) => { expect(body.annotations).to.have.property(jobId); expect(body.annotations[jobId]).to.have.length(1); - const indexedAnnotation = _.omit(body.annotations[jobId][0], '_id'); + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); }); }); - it('should not allow to fetch annotation for user with viewer permissions', async () => { + it('should not allow to fetch annotation for unauthorized user', async () => { const requestBody = { jobIds: testSetupJobConfigs.map((j) => j.job_id), earliestMs: 1454804100000, diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts index 1011bd6a4b42d..a3cbfb28aee10 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -9,6 +9,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; import { USER } from '../../../../functional/services/ml/security_common'; import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -16,47 +18,17 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); - const testSetupJobConfigs = [1, 2, 3].map((num) => ({ - job_id: `job_annotation_${num}_${Date.now()}`, - description: `Test annotation ${num}`, - groups: ['farequote', 'automated', 'single-metric'], - analysis_config: { - bucket_span: '15m', - influencers: [], - detectors: [ - { - function: 'mean', - field_name: 'responsetime', - }, - { - function: 'sum', - field_name: 'responsetime', - }, - ], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '10mb' }, - })); - const jobIds = testSetupJobConfigs.map((j) => j.job_id); - - const createAnnotationRequestBody = (jobId: string) => { - return { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Test annotation', - job_id: jobId, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'user', - detector_index: 1, - partition_field_name: 'airline', - partition_field_value: 'AAL', - }; + const commonAnnotationUpdateRequestBody: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', }; - const testSetupAnnotations = testSetupJobConfigs.map((job) => - createAnnotationRequestBody(job.job_id) - ); - describe('update_annotations', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); @@ -81,17 +53,11 @@ export default ({ getService }: FtrProviderContext) => { const originalAnnotation = annotationsForJob[0]; const annotationUpdateRequestBody = { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Updated annotation #0', + ...commonAnnotationUpdateRequestBody, job_id: originalAnnotation._source.job_id, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'model_change', - detector_index: 2, - partition_field_name: 'airline', - partition_field_value: 'ANA', _id: originalAnnotation._id, }; + const { body } = await supertest .put('/api/ml/annotations/index') .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) @@ -103,9 +69,13 @@ export default ({ getService }: FtrProviderContext) => { expect(body.result).to.eql('updated'); const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); - expect(updatedAnnotation?.annotation).to.eql(annotationUpdateRequestBody.annotation); - expect(updatedAnnotation?.detector_index).to.eql(annotationUpdateRequestBody.detector_index); - expect(updatedAnnotation?.event).to.eql(annotationUpdateRequestBody.event); + + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } }); it('should correctly update annotation for user with viewer permission', async () => { @@ -114,15 +84,16 @@ export default ({ getService }: FtrProviderContext) => { const originalAnnotation = annotationsForJob[0]; const annotationUpdateRequestBody = { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Updated annotation #1', + ...commonAnnotationUpdateRequestBody, job_id: originalAnnotation._source.job_id, type: ANNOTATION_TYPE.ANNOTATION, event: 'model_change', detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', _id: originalAnnotation._id, }; + const { body } = await supertest .put('/api/ml/annotations/index') .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) @@ -134,9 +105,12 @@ export default ({ getService }: FtrProviderContext) => { expect(body.result).to.eql('updated'); const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); - expect(updatedAnnotation?.annotation).to.eql(annotationUpdateRequestBody.annotation); - expect(updatedAnnotation?.detector_index).to.eql(annotationUpdateRequestBody.detector_index); - expect(updatedAnnotation?.event).to.eql(annotationUpdateRequestBody.event); + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } }); it('should not update annotation for unauthorized user', async () => { @@ -144,7 +118,10 @@ export default ({ getService }: FtrProviderContext) => { expect(annotationsForJob).to.have.length(1); const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + timestamp: Date.now(), + end_timestamp: Date.now(), annotation: 'Updated annotation', job_id: originalAnnotation._source.job_id, type: ANNOTATION_TYPE.ANNOTATION, @@ -154,6 +131,7 @@ export default ({ getService }: FtrProviderContext) => { partition_field_value: 'ANA', _id: originalAnnotation._id, }; + const { body } = await supertest .put('/api/ml/annotations/index') .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) @@ -167,5 +145,43 @@ export default ({ getService }: FtrProviderContext) => { const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); expect(updatedAnnotation).to.eql(originalAnnotation._source); }); + + it('should override fields correctly', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[3]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBodyWithMissingFields: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBodyWithMissingFields) + .expect(200); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => { + if (key !== '_id') { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql( + annotationUpdateRequestBodyWithMissingFields[field] + ); + } + }); + } + // validate missing fields in the annotationUpdateRequestBody + expect(updatedAnnotation?.partition_field_name).to.be(undefined); + expect(updatedAnnotation?.partition_field_value).to.be(undefined); + }); }); }; diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 7874a215dcde3..a807ad93bdd1c 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -657,7 +657,6 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const results = await es.search({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, body: { - size: 1, query: { match: { job_id: jobId, From 46e16412847c577d04cbef4dbdb09e2cb20eac1e Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 29 Jul 2020 12:55:44 -0500 Subject: [PATCH 5/6] [ML] Refactor createAnnotationRequestBody --- .../apis/ml/annotations/common_jobs.ts | 3 ++- .../apis/ml/annotations/create_annotations.ts | 15 ++------------- x-pack/test/functional/services/ml/api.ts | 4 ++-- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts index 6a52cfa57dec3..873cdc5d71baa 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -5,6 +5,7 @@ */ import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; export const commonJobConfig = { description: 'test_job_annotation', @@ -38,7 +39,7 @@ export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ })); export const jobIds = testSetupJobConfigs.map((j) => j.job_id); -const createAnnotationRequestBody = (jobId: string) => { +export const createAnnotationRequestBody = (jobId: string): Partial => { return { timestamp: Date.now(), end_timestamp: Date.now(), diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts index bb9348378c54f..a71a2959d25ed 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -9,9 +9,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; import { USER } from '../../../../functional/services/ml/security_common'; -import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; -import { createJobConfig } from './common_jobs'; +import { createJobConfig, createAnnotationRequestBody } from './common_jobs'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); @@ -20,17 +19,7 @@ export default ({ getService }: FtrProviderContext) => { const jobId = `job_annotation_${Date.now()}`; const testJobConfig = createJobConfig(jobId); - const annotationRequestBody: Partial = { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Test annotation', - job_id: jobId, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'user', - detector_index: 1, - partition_field_name: 'airline', - partition_field_value: 'AAL', - }; + const annotationRequestBody = createAnnotationRequestBody(jobId); describe('create_annotations', function () { before(async () => { diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index a807ad93bdd1c..401a96c5c11bd 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -690,10 +690,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return undefined; }, - async indexAnnotation(annotationRequestBody: Annotation) { + async indexAnnotation(annotationRequestBody: Partial) { log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); // @ts-ignore due to outdated type for IndexDocumentParams.type - const params: IndexDocumentParams = { + const params: IndexDocumentParams> = { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, body: annotationRequestBody, refresh: 'wait_for', From 63414c1f9eeca81339d1e48cf0cab7f290e98e3f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Thu, 30 Jul 2020 09:52:49 -0500 Subject: [PATCH 6/6] [ML] Fix commonAnnotationUpdateRequestBody --- .../apis/ml/annotations/create_annotations.ts | 2 +- .../apis/ml/annotations/update_annotations.ts | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts index a71a2959d25ed..14ecf1bfe524e 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -54,7 +54,7 @@ export default ({ getService }: FtrProviderContext) => { expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); }); - it('should successfully create annotation for user without required permission', async () => { + it('should successfully create annotation for user with ML read permissions', async () => { const { body } = await supertest .put('/api/ml/annotations/index') .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts index a3cbfb28aee10..ba73617151120 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -86,11 +86,6 @@ export default ({ getService }: FtrProviderContext) => { const annotationUpdateRequestBody = { ...commonAnnotationUpdateRequestBody, job_id: originalAnnotation._source.job_id, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'model_change', - detector_index: 2, - partition_field_name: 'airline', - partition_field_value: 'ANA', _id: originalAnnotation._id, }; @@ -120,15 +115,8 @@ export default ({ getService }: FtrProviderContext) => { const originalAnnotation = annotationsForJob[0]; const annotationUpdateRequestBody = { - timestamp: Date.now(), - end_timestamp: Date.now(), - annotation: 'Updated annotation', + ...commonAnnotationUpdateRequestBody, job_id: originalAnnotation._source.job_id, - type: ANNOTATION_TYPE.ANNOTATION, - event: 'model_change', - detector_index: 2, - partition_field_name: 'airline', - partition_field_value: 'ANA', _id: originalAnnotation._id, };