diff --git a/src/plugins/data_view_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/data_view_field_editor/public/lib/runtime_field_validation.ts index 770fb548f1251..7ff5b80a7464f 100644 --- a/src/plugins/data_view_field_editor/public/lib/runtime_field_validation.ts +++ b/src/plugins/data_view_field_editor/public/lib/runtime_field_validation.ts @@ -10,7 +10,7 @@ import { ScriptError } from '../components/preview/types'; import { RuntimeFieldPainlessError, PainlessErrorCode } from '../types'; export const getErrorCodeFromErrorReason = (reason: string = ''): PainlessErrorCode => { - if (reason.startsWith('Cannot cast from')) { + if (reason.includes('Cannot cast from')) { return 'CAST_ERROR'; } return 'UNKNOWN'; diff --git a/x-pack/plugins/cases/common/types/api/case/v1.test.ts b/x-pack/plugins/cases/common/types/api/case/v1.test.ts index ce27fe5070b2c..e34d4be55ef0f 100644 --- a/x-pack/plugins/cases/common/types/api/case/v1.test.ts +++ b/x-pack/plugins/cases/common/types/api/case/v1.test.ts @@ -320,6 +320,40 @@ describe('CasePostRequestRt', () => { `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` ); }); + + it('throws an error when a text customFields is an empty array', () => { + expect( + PathReporter.report( + CasePostRequestRt.decode({ + ...defaultRequest, + customFields: [ + { + key: 'first_custom_field_key', + type: 'text', + value: [], + }, + ], + }) + ) + ).toContain('The length of the field value is too short. Array must be of length >= 1.'); + }); + + it('throws an error when a text customField is an array with an empty string', () => { + expect( + PathReporter.report( + CasePostRequestRt.decode({ + ...defaultRequest, + customFields: [ + { + key: 'first_custom_field_key', + type: 'text', + value: [''], + }, + ], + }) + ) + ).toContain('The value field cannot be an empty string.'); + }); }); describe('CasesFindRequestRt', () => { diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts index f808080cc2f0d..35af579b4e99d 100644 --- a/x-pack/plugins/cases/common/types/api/case/v1.ts +++ b/x-pack/plugins/cases/common/types/api/case/v1.ts @@ -44,21 +44,21 @@ import { CaseConnectorRt } from '../../domain/connector/v1'; import { CaseUserProfileRt, UserRt } from '../../domain/user/v1'; import { CasesStatusResponseRt } from '../stats/v1'; -const CaseCustomFieldWithValidationValueRt = limitedArraySchema({ +const CaseCustomFieldTextWithValidationValueRt = limitedArraySchema({ codec: limitedStringSchema({ fieldName: 'value', - min: 0, + min: 1, max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH, }), fieldName: 'value', - min: 0, + min: 1, max: MAX_CUSTOM_FIELD_TEXT_VALUE_ITEMS, }); const CaseCustomFieldTextWithValidationRt = rt.strict({ key: rt.string, type: CustomFieldTextTypeRt, - value: rt.union([CaseCustomFieldWithValidationValueRt, rt.null]), + value: rt.union([CaseCustomFieldTextWithValidationValueRt, rt.null]), }); const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index ae31b77058e1e..4704f9002e33b 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -570,6 +570,52 @@ describe('create', () => { ); }); + it('should throw an error when required customFields are null', async () => { + casesClient.configure.get = jest.fn().mockResolvedValue([ + { + owner: theCase.owner, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'foo', + required: true, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + required: true, + }, + ], + }, + ]); + + await expect( + create( + { + ...theCase, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + value: null, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + value: null, + }, + ], + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to create case: Error: Missing required custom fields: first_key,second_key"` + ); + }); + it('throws error when the customFields array is too long', async () => { await expect( create( diff --git a/x-pack/plugins/cases/server/client/cases/validators.test.ts b/x-pack/plugins/cases/server/client/cases/validators.test.ts index 6956440a25685..d9ba08ac63630 100644 --- a/x-pack/plugins/cases/server/client/cases/validators.test.ts +++ b/x-pack/plugins/cases/server/client/cases/validators.test.ts @@ -305,7 +305,7 @@ describe('validators', () => { { key: 'second_key', type: CustomFieldTypes.TOGGLE as const, - value: null, + value: true, }, ]; expect(() => @@ -373,6 +373,38 @@ describe('validators', () => { ).toThrowErrorMatchingInlineSnapshot(`"Missing required custom fields: first_key"`); }); + it('throws if required custom fields have null value', () => { + const requestCustomFields: CaseCustomFields = [ + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + value: null, + }, + ]; + const customFieldsConfiguration: CustomFieldsConfiguration = [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'foo', + required: true, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + required: true, + }, + ]; + expect(() => + validateRequiredCustomFields({ + requestCustomFields, + customFieldsConfiguration, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required custom fields: first_key,second_key"` + ); + }); + it('throws if configuration is missing and request has custom fields', () => { const requestCustomFields: CaseCustomFields = [ { diff --git a/x-pack/plugins/cases/server/client/cases/validators.ts b/x-pack/plugins/cases/server/client/cases/validators.ts index f33ac2ff4d087..87e677dca457b 100644 --- a/x-pack/plugins/cases/server/client/cases/validators.ts +++ b/x-pack/plugins/cases/server/client/cases/validators.ts @@ -107,12 +107,26 @@ export const validateRequiredCustomFields = ({ (customField) => customField.required ); + if (!requiredCustomFields.length) { + return; + } + const missingRequiredCustomFields = differenceWith( requiredCustomFields, requestCustomFields ?? [], (requiredVal, requestedVal) => requiredVal.key === requestedVal.key ).map((e) => e.key); + requiredCustomFields.forEach((requiredField) => { + const found = requestCustomFields?.find( + (requestField) => requestField.key === requiredField.key + ); + + if (found && found.value === null) { + missingRequiredCustomFields.push(found.key); + } + }); + if (missingRequiredCustomFields.length) { throw Boom.badRequest(`Missing required custom fields: ${missingRequiredCustomFields}`); } diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 576fc11cdf51b..246b9c5c12131 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -1085,6 +1085,54 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when trying to patch a case with a required custom field with null value', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: true, + }, + ], + }, + }) + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: [ + { + key: 'test_custom_field', + type: CustomFieldTypes.TEXT, + value: ['hello'], + }, + ], + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + customFields: [ + { + key: 'test_custom_field', + type: CustomFieldTypes.TEXT, + value: null, + }, + ], + }, + ], + }, + expectedHttpCode: 400, + }); + }); it('400s when trying to patch a case with a custom field with the wrong type', async () => { await createConfiguration( supertest, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 6d152008e9074..3272a778ee5f1 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -482,14 +482,14 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - it('400s when trying to create case with a required custom field', async () => { + it('400s when creating a case with a missing required custom field', async () => { await createConfiguration( supertest, getConfigurationRequest({ overrides: { customFields: [ { - key: 'test_custom_field', + key: 'text_custom_field', label: 'text', type: CustomFieldTypes.TEXT, required: false, @@ -509,9 +509,40 @@ export default ({ getService }: FtrProviderContext): void => { getPostCaseRequest({ customFields: [ { - key: 'test_custom_field', - type: CustomFieldTypes.TOGGLE, - value: true, + key: 'text_custom_field', + type: CustomFieldTypes.TEXT, + value: ['a'], + }, + ], + }), + 400 + ); + }); + + it('400s when trying to create case with a required custom field as null', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'text_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: true, + }, + ], + }, + }) + ); + await createCase( + supertest, + getPostCaseRequest({ + customFields: [ + { + key: 'text_custom_field', + type: CustomFieldTypes.TEXT, + value: null, }, ], }), diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts index 9580108506c0a..9ed30298a9c3f 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts @@ -48,8 +48,7 @@ export default function ({ getService }: FtrProviderContext) { }); }; - // Failing: See https://github.com/elastic/kibana/issues/165868 - describe.skip('Field preview', function () { + describe('Field preview', function () { before(async () => await createIndex()); after(async () => await deleteIndex()); @@ -149,10 +148,7 @@ export default function ({ getService }: FtrProviderContext) { // As ES does not return error codes we will add a test to make sure its error message string // does not change overtime as we rely on it to extract our own error code. // If this test fail we'll need to update the "getErrorCodeFromErrorReason()" handler - // TODO: `response.error?.caused_by?.reason` returns - // `class_cast_exception: Cannot cast from [int] to [java.lang.String].` - // in Serverless, which causes `getErrorCodeFromErrorReason` to fail - it.skip('should detect a script casting error', async () => { + it('should detect a script casting error', async () => { const { body: response } = await supertest .post(FIELD_PREVIEW_PATH) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)