Skip to content

Commit

Permalink
[ML] Add API tests for analytics jobs_exist and new_job_caps endpoints (
Browse files Browse the repository at this point in the history
#126914)

* [ML] Add API tests for analytics jobs_exist and new_job_caps endpoints

* [ML] Edits following review

* [ML] Edit to api doc

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
peteharverson and kibanamachine authored Mar 7, 2022
1 parent 8b82657 commit d0b64d9
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 5 deletions.
10 changes: 5 additions & 5 deletions x-pack/plugins/ml/server/routes/data_frame_analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,12 +609,12 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout
/**
* @apiGroup DataFrameAnalytics
*
* @api {post} /api/ml/data_frame/analytics/job_exists Check whether jobs exists in current or any space
* @apiName JobExists
* @apiDescription Checks if each of the jobs in the specified list of IDs exist.
* @api {post} /api/ml/data_frame/analytics/jobs_exist Check whether jobs exist in current or any space
* @apiName JobsExist
* @apiDescription Checks if each of the jobs in the specified list of IDs exists.
* If allSpaces is true, the check will look across all spaces.
*
* @apiSchema (params) analyticsIdSchema
* @apiSchema (params) jobsExistSchema
*/
router.post(
{
Expand Down Expand Up @@ -707,7 +707,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout
/**
* @apiGroup DataFrameAnalytics
*
* @api {get} api/data_frame/analytics/fields/:indexPattern Get fields for a pattern of indices used for analytics
* @api {get} /api/ml/data_frame/analytics/new_job_caps/:indexPattern Get fields for a pattern of indices used for analytics
* @apiName AnalyticsNewJobCaps
* @apiDescription Retrieve the index fields for analytics
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./delete_spaces'));
loadTestFile(require.resolve('./evaluate'));
loadTestFile(require.resolve('./explain'));
loadTestFile(require.resolve('./jobs_exist_spaces'));
loadTestFile(require.resolve('./new_job_caps'));
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import expect from '@kbn/expect';

import { FtrProviderContext } from '../../../ftr_provider_context';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
import { USER } from '../../../../functional/services/ml/security_common';

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

const jobIdSpace1 = 'ihp_od_space1';
const jobIdSpace2 = 'ihp_od_space2';
const idSpace1 = 'space1';
const idSpace2 = 'space2';

const initialModelMemoryLimit = '17mb';

async function runRequest(
space: string,
expectedStatusCode: number,
analyticsIds?: string[],
allSpaces?: boolean
) {
const { body } = await supertest
.post(`/s/${space}/api/ml/data_frame/analytics/jobs_exist`)
.auth(
USER.ML_VIEWER_ALL_SPACES,
ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES)
)
.set(COMMON_REQUEST_HEADERS)
.send(allSpaces ? { analyticsIds, allSpaces } : { analyticsIds })
.expect(expectedStatusCode);

return body;
}

describe('POST data_frame/analytics/jobs_exist with spaces', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier');
await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] });
await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] });

const jobConfigSpace1 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace1);
await ml.api.createDataFrameAnalyticsJob(
{ ...jobConfigSpace1, model_memory_limit: initialModelMemoryLimit },
idSpace1
);

const jobConfigSpace2 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace2);
await ml.api.createDataFrameAnalyticsJob(
{ ...jobConfigSpace2, model_memory_limit: initialModelMemoryLimit },
idSpace2
);

await ml.testResources.setKibanaTimeZoneToUTC();
});

after(async () => {
await spacesService.delete(idSpace1);
await spacesService.delete(idSpace2);
await ml.api.cleanMlIndices();
await ml.testResources.cleanMLSavedObjects();
});

it('should find single job from same space', async () => {
const body = await runRequest(idSpace1, 200, [jobIdSpace1]);
expect(body).to.eql({ [jobIdSpace1]: { exists: true } });
});

it('should not find single job from different space', async () => {
const body = await runRequest(idSpace2, 200, [jobIdSpace1]);
expect(body).to.eql({ [jobIdSpace1]: { exists: false } });
});

it('should only find job from same space when called with a list of jobs', async () => {
const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]);
expect(body).to.eql({
[jobIdSpace1]: { exists: true },
[jobIdSpace2]: { exists: false },
});
});

it('should find single job from different space when run across all spaces', async () => {
const body = await runRequest(idSpace1, 200, [jobIdSpace2], true);
expect(body).to.eql({ [jobIdSpace2]: { exists: true } });
});
});
};
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import expect from '@kbn/expect';

import { FtrProviderContext } from '../../../ftr_provider_context';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
import { USER } from '../../../../functional/services/ml/security_common';

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

async function runRequest(indexPattern: string, expectedStatusCode: number, rollup?: boolean) {
let url = `/api/ml/data_frame/analytics/new_job_caps/${indexPattern}`;
if (rollup !== undefined) {
url += `?rollup=${rollup}`;
}
const { body } = await supertest
.get(url)
.auth(
USER.ML_VIEWER_ALL_SPACES,
ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES)
)
.set(COMMON_REQUEST_HEADERS)
.expect(expectedStatusCode);

return body;
}

describe('GET data_frame/analytics/new_job_caps', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification');
await ml.testResources.setKibanaTimeZoneToUTC();
});

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

it('should return job capabilities of fields for an index that exists', async () => {
const body = await runRequest(testIndexPattern, 200);
await ml.testExecution.logTestStep(
`response should contain object for ${testIndexPattern} index pattern`
);
expect(body).to.have.keys(testIndexPattern);
const testIndexPatternCaps = body[testIndexPattern];

// The data frame analytics UI does not use the aggs prop, so just perform basic checks this prop
await ml.testExecution.logTestStep(
`should contain aggs and fields props for ${testIndexPattern} index pattern`
);
expect(testIndexPatternCaps).to.have.keys('aggs', 'fields');
const aggs = testIndexPatternCaps.aggs;
expect(aggs).to.have.length(35);

// The data frames analytics UI uses this endpoint to extract the names and types of fields,
// so check this info is present for some example fields
const fields = testIndexPatternCaps.fields;
expect(fields).to.have.length(24);

await ml.testExecution.logTestStep(
`fields should contain expected name and type attributes for ${testIndexPattern} index pattern`
);
const balanceTextField = fields.find((obj: any) => obj.id === 'balance');
expect(balanceTextField).to.have.keys('name', 'type');
expect(balanceTextField.name).to.eql('balance');
expect(balanceTextField.type).to.eql('text');

const balanceKeywordField = fields.find((obj: any) => obj.id === 'balance.keyword');
expect(balanceKeywordField).to.have.keys('name', 'type');
expect(balanceKeywordField.name).to.eql('balance.keyword');
expect(balanceKeywordField.type).to.eql('keyword');
});

it('should fail to return job capabilities of fields for an index that does not exist', async () => {
await runRequest(`${testIndexPattern}_invalid`, 404);
});

it('should return empty job capabilities of fields for a non-rollup index with rollup parameter set to true', async () => {
const body = await runRequest(testIndexPattern, 200, true);
await ml.testExecution.logTestStep(
`response should contain object for ${testIndexPattern} index pattern`
);
expect(body).to.have.keys(testIndexPattern);
const testIndexPatternCaps = body[testIndexPattern];

await ml.testExecution.logTestStep(
`should contain empty aggs and fields props for ${testIndexPattern} index pattern`
);
expect(testIndexPatternCaps).to.have.keys('aggs', 'fields');
const aggs = testIndexPatternCaps.aggs;
expect(aggs).to.have.length(0);
const fields = testIndexPatternCaps.fields;
expect(fields).to.have.length(0);
});
});
};

0 comments on commit d0b64d9

Please sign in to comment.