Skip to content

Commit

Permalink
[ML] Add API integration testing for AD annotations (#73068)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
qn895 and elasticmachine authored Jul 31, 2020
1 parent b0d51c8 commit 69844e4
Show file tree
Hide file tree
Showing 8 changed files with 648 additions and 0 deletions.
58 changes: 58 additions & 0 deletions x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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';
import { Annotation } from '../../../../../plugins/ml/common/types/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);

export const createAnnotationRequestBody = (jobId: string): Partial<Annotation> => {
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)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 } from '../../../../../plugins/ml/common/types/annotations';
import { createJobConfig, createAnnotationRequestBody } from './common_jobs';
// 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 = createJobConfig(jobId);
const annotationRequestBody = createAnnotationRequestBody(jobId);

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).to.not.be(undefined);

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);
});

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))
.set(COMMON_REQUEST_HEADERS)
.send(annotationRequestBody)
.expect(200);

const annotationId = body._id;
const fetchedAnnotation = await ml.api.getAnnotationById(annotationId);
expect(fetchedAnnotation).to.not.be(undefined);
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);
});

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');
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs';

// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const ml = getService('ml');

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);
});
});
};
130 changes: 130 additions & 0 deletions x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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 { 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 { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs';

// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const ml = getService('ml');

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,
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 ML read 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 unauthorized user', 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');
});
});
};
16 changes: 16 additions & 0 deletions x-pack/test/api_integration/apis/ml/annotations/index.ts
Original file line number Diff line number Diff line change
@@ -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'));
});
}
Loading

0 comments on commit 69844e4

Please sign in to comment.