Skip to content

Commit

Permalink
[Cases] Required custom fields in the create case API (#167131)
Browse files Browse the repository at this point in the history
## Summary

This PR checks for missing required custom fields in the create case
API.

(In my next PR I am thinking of moving `casesClient.configure.get({
owner: casePostRequest.owner });` outside of the validation functions.)
  • Loading branch information
adcoelho authored Sep 25, 2023
1 parent 71b177f commit 7307c06
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 1 deletion.
129 changes: 128 additions & 1 deletion x-pack/plugins/cases/server/client/cases/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { CasePostRequest } from '../../../common';
import { SECURITY_SOLUTION_OWNER } from '../../../common';
import { mockCases } from '../../mocks';
import { createCasesClientMock, createCasesClientMockArgs } from '../mocks';
import { create, throwIfCustomFieldKeysInvalid } from './create';
import { create, throwIfCustomFieldKeysInvalid, throwIfMissingRequiredCustomField } from './create';
import {
CaseSeverity,
CaseStatuses,
Expand Down Expand Up @@ -702,4 +702,131 @@ describe('create', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`"No custom fields configured."`);
});
});

describe('throwIfMissingRequiredCustomField', () => {
const casesClient = createCasesClientMock();

beforeEach(() => {
jest.clearAllMocks();
});

it('does not throw if all required custom fields are in the request', async () => {
const customFields = [
{
key: 'first_key',
type: CustomFieldTypes.TEXT as const,
field: { value: ['this is a text field value', 'this is second'] },
},
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE as const,
field: { value: null },
},
];

casesClient.configure.get = jest.fn().mockResolvedValue([
{
owner: mockCases[0].attributes.owner,
customFields: [
{
key: 'first_key',
type: CustomFieldTypes.TEXT,
label: 'foo',
},
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE,
label: 'foo',
},
],
},
]);

await expect(
throwIfMissingRequiredCustomField({
casePostRequest: {
customFields,
} as unknown as CasePostRequest,
casesClient,
})
).resolves.not.toThrow();
});

it('does not throw if there are only optional custom fields in configuration', async () => {
casesClient.configure.get = jest.fn().mockResolvedValue([
{
owner: mockCases[0].attributes.owner,
customFields: [
{
key: 'first_key',
type: CustomFieldTypes.TEXT,
label: 'foo',
required: false,
},
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE,
label: 'foo',
required: false,
},
],
},
]);

await expect(
throwIfMissingRequiredCustomField({
casePostRequest: {} as unknown as CasePostRequest,
casesClient,
})
).resolves.not.toThrow();
});

it('does not throw if no configuration found', async () => {
casesClient.configure.get = jest.fn().mockResolvedValue([]);

await expect(
throwIfMissingRequiredCustomField({
casePostRequest: {} as unknown as CasePostRequest,
casesClient,
})
).resolves.not.toThrow();
});

it('throws if the request has missing required custom fields', async () => {
casesClient.configure.get = jest.fn().mockResolvedValue([
{
owner: mockCases[0].attributes.owner,
customFields: [
{
key: 'first_key',
type: CustomFieldTypes.TEXT,
label: 'foo',
required: true,
},
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE,
label: 'foo',
required: true,
},
],
},
]);

await expect(
throwIfMissingRequiredCustomField({
casePostRequest: {
customFields: [
{
key: 'second_key',
type: CustomFieldTypes.TOGGLE,
label: 'foo',
},
],
} as unknown as CasePostRequest,
casesClient,
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Missing required custom fields: first_key"`);
});
});
});
39 changes: 39 additions & 0 deletions x-pack/plugins/cases/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Boom from '@hapi/boom';

import { SavedObjectsUtils } from '@kbn/core/server';

import { differenceWith } from 'lodash';
import type { Case } from '../../../common/types/domain';
import { CaseSeverity, UserActionTypes, CaseRt } from '../../../common/types/domain';
import { decodeWithExcessOrThrow } from '../../../common/api';
Expand Down Expand Up @@ -56,6 +57,43 @@ export async function throwIfCustomFieldKeysInvalid({
}
}

/**
* Throws if there are required custom fields missing in the request.
*/
export async function throwIfMissingRequiredCustomField({
casePostRequest,
casesClient,
}: {
casePostRequest: CasePostRequest;
casesClient: CasesClient;
}) {
const requestCustomFields = casePostRequest.customFields;

if (!Array.isArray(requestCustomFields) || !requestCustomFields.length) {
return;
}

const configuration = await casesClient.configure.get({ owner: casePostRequest.owner });

if (configuration.length === 0) {
return;
}

const requiredCustomFields = configuration[0].customFields.filter(
(customField) => customField.required
);

const invalidCustomFieldKeys = differenceWith(
requiredCustomFields,
requestCustomFields,
(requiredVal, requestedVal) => requiredVal.key === requestedVal.key
).map((e) => e.key);

if (invalidCustomFieldKeys.length) {
throw Boom.badRequest(`Missing required custom fields: ${invalidCustomFieldKeys}`);
}
}

/**
* Creates a new case.
*
Expand All @@ -78,6 +116,7 @@ export const create = async (

throwIfDuplicatedCustomFieldKeysInRequest({ customFieldsInRequest: query.customFields });
await throwIfCustomFieldKeysInvalid({ casePostRequest: query, casesClient });
await throwIfMissingRequiredCustomField({ casePostRequest: query, casesClient });

const savedObjectID = SavedObjectsUtils.generateId();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,43 @@ export default ({ getService }: FtrProviderContext): void => {
400
);
});

it('400s when trying to create case with a missing required custom field', async () => {
await createConfiguration(
supertest,
getConfigurationRequest({
overrides: {
customFields: [
{
key: 'test_custom_field',
label: 'text',
type: CustomFieldTypes.TEXT,
required: true,
},
{
key: 'toggle_custom_field',
label: 'toggle',
type: CustomFieldTypes.TOGGLE,
required: true,
},
],
},
})
);
await createCase(
supertest,
getPostCaseRequest({
customFields: [
{
key: 'toggle_custom_field',
type: CustomFieldTypes.TOGGLE,
field: { value: [true] },
},
],
}),
400
);
});
});
});

Expand Down

0 comments on commit 7307c06

Please sign in to comment.