Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] API integration tests for UPDATE data frame analytics endpoint #72710

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('data frame analytics', function () {
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./update'));
});
}
275 changes: 275 additions & 0 deletions x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/*
* 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 { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common';
import { DeepPartial } from '../../../../../plugins/ml/common/types/common';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common';

export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const ml = getService('ml');

const jobId = `bm_${Date.now()}`;
const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`;
const commonJobConfig = {
source: {
index: ['ft_bank_marketing'],
query: {
match_all: {},
},
},
analysis: {
classification: {
dependent_variable: 'y',
training_percent: 20,
},
},
analyzed_fields: {
includes: [],
excludes: [],
},
model_memory_limit: '60mb',
allow_lazy_start: false, // default value
max_num_threads: 1, // default value
};

const testJobConfigs: Array<DeepPartial<DataFrameAnalyticsConfig>> = [
'Test update job',
'Test update job description only',
'Test update job allow_lazy_start only',
'Test update job model_memory_limit only',
'Test update job max_num_threads only',
].map((description, idx) => {
const analyticsId = `${jobId}_${idx}`;
return {
id: analyticsId,
description,
dest: {
index: generateDestinationIndex(analyticsId),
results_field: 'ml',
},
...commonJobConfig,
};
});

const editedDescription = 'Edited description';

async function createJobs(mockJobConfigs: Array<DeepPartial<DataFrameAnalyticsConfig>>) {
for (const jobConfig of mockJobConfigs) {
await ml.api.createDataFrameAnalyticsJob(jobConfig as DataFrameAnalyticsConfig);
}
}

async function getDFAJob(id: string) {
const { body } = await supertest
.get(`/api/ml/data_frame/analytics/${id}`)
.auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
.set(COMMON_REQUEST_HEADERS);

return body.data_frame_analytics[0];
}

describe('UPDATE data_frame/analytics', () => {
before(async () => {
await esArchiver.loadIfNeeded('ml/bm_classification');
await ml.testResources.setKibanaTimeZoneToUTC();
await createJobs(testJobConfigs);
});

after(async () => {
await ml.api.cleanMlIndices();
});

describe('UpdateDataFrameAnalytics', () => {
it('should update all editable fields of analytics job for specified id', async () => {
const analyticsId = `${jobId}_0`;

const requestBody = {
description: editedDescription,
model_memory_limit: '61mb',
allow_lazy_start: true,
max_num_threads: 2,
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(200);

expect(body).not.to.be(undefined);

const fetchedJob = await getDFAJob(analyticsId);

expect(fetchedJob.description).to.eql(requestBody.description);
expect(fetchedJob.allow_lazy_start).to.eql(requestBody.allow_lazy_start);
expect(fetchedJob.model_memory_limit).to.eql(requestBody.model_memory_limit);
expect(fetchedJob.max_num_threads).to.eql(requestBody.max_num_threads);
});

it('should only update description field of analytics job when description is sent in request', async () => {
const analyticsId = `${jobId}_1`;

const requestBody = {
description: 'Edited description for job 1',
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(200);

expect(body).not.to.be(undefined);

const fetchedJob = await getDFAJob(analyticsId);

expect(fetchedJob.description).to.eql(requestBody.description);
expect(fetchedJob.allow_lazy_start).to.eql(commonJobConfig.allow_lazy_start);
expect(fetchedJob.model_memory_limit).to.eql(commonJobConfig.model_memory_limit);
expect(fetchedJob.max_num_threads).to.eql(commonJobConfig.max_num_threads);
});

it('should only update allow_lazy_start field of analytics job when allow_lazy_start is sent in request', async () => {
const analyticsId = `${jobId}_2`;

const requestBody = {
allow_lazy_start: true,
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(200);

expect(body).not.to.be(undefined);

const fetchedJob = await getDFAJob(analyticsId);

expect(fetchedJob.allow_lazy_start).to.eql(requestBody.allow_lazy_start);
expect(fetchedJob.description).to.eql(testJobConfigs[2].description);
expect(fetchedJob.model_memory_limit).to.eql(commonJobConfig.model_memory_limit);
expect(fetchedJob.max_num_threads).to.eql(commonJobConfig.max_num_threads);
});

it('should only update model_memory_limit field of analytics job when model_memory_limit is sent in request', async () => {
const analyticsId = `${jobId}_3`;

const requestBody = {
model_memory_limit: '61mb',
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(200);

expect(body).not.to.be(undefined);

const fetchedJob = await getDFAJob(analyticsId);

expect(fetchedJob.model_memory_limit).to.eql(requestBody.model_memory_limit);
expect(fetchedJob.allow_lazy_start).to.eql(commonJobConfig.allow_lazy_start);
expect(fetchedJob.description).to.eql(testJobConfigs[3].description);
expect(fetchedJob.max_num_threads).to.eql(commonJobConfig.max_num_threads);
});

it('should only update max_num_threads field of analytics job when max_num_threads is sent in request', async () => {
const analyticsId = `${jobId}_4`;

const requestBody = {
max_num_threads: 2,
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(200);

expect(body).not.to.be(undefined);

const fetchedJob = await getDFAJob(analyticsId);

expect(fetchedJob.max_num_threads).to.eql(requestBody.max_num_threads);
expect(fetchedJob.model_memory_limit).to.eql(commonJobConfig.model_memory_limit);
expect(fetchedJob.allow_lazy_start).to.eql(commonJobConfig.allow_lazy_start);
expect(fetchedJob.description).to.eql(testJobConfigs[4].description);
});

it('should not allow to update analytics job for unauthorized user', async () => {
const analyticsId = `${jobId}_0`;
const requestBody = {
description: 'Unauthorized',
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.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');
pheyos marked this conversation as resolved.
Show resolved Hide resolved

const fetchedJob = await getDFAJob(analyticsId);
// Description should not have changed
expect(fetchedJob.description).to.eql(editedDescription);
Copy link
Member

@qn895 qn895 Jul 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance it would be hard to know fetchedJob.description should equal editedDescription based on the variable name, even though I know we already made a request to update the job to that editedDescription earlier. I wonder if it would be clearer to expect fetchedJob.description to not eql requestBody.description

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer to have an explicit check for the description so we know it hasn't been changed in any way. A not equal check could potentially miss any undesired changes to the description.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @alvarezmelissa87 - we should stay with the explicit checks. The only thing that comes to my mind for making it more clear, is to maybe rename fetchedJob to updatedJob?

});

it('should not allow to update analytics job for the user with only view permission', async () => {
const analyticsId = `${jobId}_0`;
const requestBody = {
description: 'View only',
};

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_update`)
.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');
pheyos marked this conversation as resolved.
Show resolved Hide resolved

const fetchedJob = await getDFAJob(analyticsId);
// Description should not have changed
expect(fetchedJob.description).to.eql(editedDescription);
});

it('should show 404 error if job does not exist', async () => {
const requestBody = {
description: 'Not found',
};
const id = `${jobId}_invalid`;
const message = `[resource_not_found_exception] No known data frame analytics with id [${id}]`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${id}/_update`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.send(requestBody)
.expect(404);

expect(body.error).to.eql('Not Found');
expect(body.message).to.eql(message);
});
});
});
};