diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index ccafe70406ec..34551b74d8c9 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -8,6 +8,9 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { getCreateCommentsArrayMock } from '../types/create_comments.mock'; +import { getCommentsMock } from '../types/comments.mock'; +import { CommentsArray } from '../types'; import { CreateExceptionListItemSchema, @@ -26,7 +29,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not accept an undefined for "description"', () => { + test('it should not validate an undefined for "description"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.description; const decoded = createExceptionListItemSchema.decode(payload); @@ -38,7 +41,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not accept an undefined for "name"', () => { + test('it should not validate an undefined for "name"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.name; const decoded = createExceptionListItemSchema.decode(payload); @@ -50,7 +53,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not accept an undefined for "type"', () => { + test('it should not validate an undefined for "type"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.type; const decoded = createExceptionListItemSchema.decode(payload); @@ -62,7 +65,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not accept an undefined for "list_id"', () => { + test('it should not validate an undefined for "list_id"', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.list_id; const decoded = createExceptionListItemSchema.decode(inputPayload); @@ -74,7 +77,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should accept an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { + test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { const payload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete payload.meta; @@ -87,7 +90,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.comments; @@ -100,7 +103,34 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "entries" but return an array', () => { + test('it should validate "comments" array', () => { + const inputPayload = { + ...getCreateExceptionListItemSchemaMock(), + comments: getCreateCommentsArrayMock(), + }; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(inputPayload); + }); + + test('it should NOT validate "comments" with "created_at" or "created_by" values', () => { + const inputPayload: Omit & { + comments?: CommentsArray; + } = { + ...getCreateExceptionListItemSchemaMock(), + comments: [getCommentsMock()], + }; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate an undefined for "entries" but return an array', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.entries; @@ -113,7 +143,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => { + test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.namespace_type; @@ -126,7 +156,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.tags; @@ -139,7 +169,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload._tags; @@ -152,7 +182,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should accept an undefined for "item_id" and auto generate a uuid', () => { + test('it should validate an undefined for "item_id" and auto generate a uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.item_id; const decoded = createExceptionListItemSchema.decode(inputPayload); @@ -164,7 +194,7 @@ describe('create_exception_list_item_schema', () => { ); }); - test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => { + test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.item_id; const decoded = createExceptionListItemSchema.decode(inputPayload); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index f593b5d16403..fb452ac89576 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -23,7 +23,7 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types'; +import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types'; import { EntriesArray } from '../types/entries'; import { DefaultUuid } from '../../siem_common_deps'; @@ -39,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode - comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode + comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode meta, // defaults to undefined if not set during decode @@ -63,7 +63,7 @@ export type CreateExceptionListItemSchemaDecoded = Identity< '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' > & { _tags: _Tags; - comments: CommentsPartialArray; + comments: CreateCommentsArray; tags: Tags; item_id: ItemId; entries: EntriesArray; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index c32b15fecb57..582fabdc160f 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -23,10 +23,10 @@ import { } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; import { - CommentsPartialArray, - DefaultCommentsPartialArray, DefaultEntryArray, + DefaultUpdateCommentsArray, EntriesArray, + UpdateCommentsArray, } from '../types'; export const updateExceptionListItemSchema = t.intersection([ @@ -40,7 +40,7 @@ export const updateExceptionListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode - comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode + comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode id, // defaults to undefined if not set during decode item_id: t.union([t.string, t.undefined]), @@ -65,7 +65,7 @@ export type UpdateExceptionListItemSchemaDecoded = Identity< '_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments' > & { _tags: _Tags; - comments: CommentsPartialArray; + comments: UpdateCommentsArray; tags: Tags; entries: EntriesArray; namespace_type: NamespaceType; diff --git a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/comments.mock.ts index ee58fafe074c..9e56ac292f8b 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comments.mock.ts @@ -6,17 +6,12 @@ import { DATE_NOW, USER } from '../../constants.mock'; -import { CommentsArray } from './comments'; +import { Comments, CommentsArray } from './comments'; -export const getCommentsMock = (): CommentsArray => [ - { - comment: 'some comment', - created_at: DATE_NOW, - created_by: USER, - }, - { - comment: 'some other comment', - created_at: DATE_NOW, - created_by: 'lily', - }, -]; +export const getCommentsMock = (): Comments => ({ + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, +}); + +export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments.test.ts b/x-pack/plugins/lists/common/schemas/types/comments.test.ts new file mode 100644 index 000000000000..29bfde03abcc --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments.test.ts @@ -0,0 +1,217 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { DATE_NOW } from '../../constants.mock'; +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getCommentsArrayMock, getCommentsMock } from './comments.mock'; +import { + Comments, + CommentsArray, + CommentsArrayOrUndefined, + comments, + commentsArray, + commentsArrayOrUndefined, +} from './comments'; + +describe('Comments', () => { + describe('comments', () => { + test('it should validate a comments', () => { + const payload = getCommentsMock(); + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate with "updated_at" and "updated_by"', () => { + const payload = getCommentsMock(); + payload.updated_at = DATE_NOW; + payload.updated_by = 'someone'; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when undefined', () => { + const payload = undefined; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"', + 'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCommentsMock(), + comment: ['some value'], + }; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "created_at" is not a string', () => { + const payload: Omit & { created_at: number } = { + ...getCommentsMock(), + created_at: 1, + }; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "created_by" is not a string', () => { + const payload: Omit & { created_by: number } = { + ...getCommentsMock(), + created_by: 1, + }; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "updated_at" is not a string', () => { + const payload: Omit & { updated_at: number } = { + ...getCommentsMock(), + updated_at: 1, + }; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "updated_by" is not a string', () => { + const payload: Omit & { updated_by: number } = { + ...getCommentsMock(), + updated_by: 1, + }; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: Comments & { + extraKey?: string; + } = getCommentsMock(); + payload.extraKey = 'some value'; + const decoded = comments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCommentsMock()); + }); + }); + + describe('commentsArray', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when a comments includes "updated_at" and "updated_by"', () => { + const commentsPayload = getCommentsMock(); + commentsPayload.updated_at = DATE_NOW; + commentsPayload.updated_by = 'someone'; + const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()]; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when undefined', () => { + const payload = undefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CommentsArray; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('commentsArrayOrUndefined', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CommentsArrayOrUndefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/comments.ts b/x-pack/plugins/lists/common/schemas/types/comments.ts index d61608c3508f..0ee3b05c8102 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.ts +++ b/x-pack/plugins/lists/common/schemas/types/comments.ts @@ -5,36 +5,24 @@ */ import * as t from 'io-ts'; -export const comment = t.exact( - t.type({ - comment: t.string, - created_at: t.string, // TODO: Make this into an ISO Date string check, - created_by: t.string, - }) -); - -export const commentsArray = t.array(comment); -export type CommentsArray = t.TypeOf; -export type Comment = t.TypeOf; -export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); -export type CommentsArrayOrUndefined = t.TypeOf; - -export const commentPartial = t.intersection([ +export const comments = t.intersection([ t.exact( t.type({ comment: t.string, - }) - ), - t.exact( - t.partial({ created_at: t.string, // TODO: Make this into an ISO Date string check, created_by: t.string, }) ), + t.exact( + t.partial({ + updated_at: t.string, + updated_by: t.string, + }) + ), ]); -export const commentsPartialArray = t.array(commentPartial); -export type CommentsPartialArray = t.TypeOf; -export type CommentPartial = t.TypeOf; -export const commentsPartialArrayOrUndefined = t.union([commentsPartialArray, t.undefined]); -export type CommentsPartialArrayOrUndefined = t.TypeOf; +export const commentsArray = t.array(comments); +export type CommentsArray = t.TypeOf; +export type Comments = t.TypeOf; +export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); +export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts new file mode 100644 index 000000000000..60a59432275c --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts @@ -0,0 +1,12 @@ +/* + * 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 { CreateComments, CreateCommentsArray } from './create_comments'; + +export const getCreateCommentsMock = (): CreateComments => ({ + comment: 'some comments', +}); + +export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()]; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts new file mode 100644 index 000000000000..d2680750e05e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comments.mock'; +import { + CreateComments, + CreateCommentsArray, + CreateCommentsArrayOrUndefined, + createComments, + createCommentsArray, + createCommentsArrayOrUndefined, +} from './create_comments'; + +describe('CreateComments', () => { + describe('createComments', () => { + test('it should validate a comments', () => { + const payload = getCreateCommentsMock(); + const decoded = createComments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when undefined', () => { + const payload = undefined; + const decoded = createComments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "{| comment: string |}"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCreateCommentsMock(), + comment: ['some value'], + }; + const decoded = createComments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: CreateComments & { + extraKey?: string; + } = getCreateCommentsMock(); + payload.extraKey = 'some value'; + const decoded = createComments.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCreateCommentsMock()); + }); + }); + + describe('createCommentsArray', () => { + test('it should validate an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when undefined', () => { + const payload = undefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<{| comment: string |}>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArray; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('createCommentsArrayOrUndefined', () => { + test('it should validate an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.ts new file mode 100644 index 000000000000..c34419298ef9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.ts @@ -0,0 +1,18 @@ +/* + * 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 * as t from 'io-ts'; + +export const createComments = t.exact( + t.type({ + comment: t.string, + }) +); + +export const createCommentsArray = t.array(createComments); +export type CreateCommentsArray = t.TypeOf; +export type CreateComments = t.TypeOf; +export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); +export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts new file mode 100644 index 000000000000..3a4241aaec82 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DefaultCommentsArray } from './default_comments_array'; +import { CommentsArray } from './comments'; +import { getCommentsArrayMock } from './comments.mock'; + +describe('default_comments_array', () => { + test('it should validate an empty array', () => { + const payload: CommentsArray = []; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of comments', () => { + const payload: CommentsArray = getCommentsArrayMock(); + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + // TODO: Known weird error formatting that is on our list to address + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts index e824d481b361..e8be299246ab 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts @@ -7,14 +7,9 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { CommentsArray, CommentsPartialArray, comment, commentPartial } from './comments'; +import { CommentsArray, comments } from './comments'; export type DefaultCommentsArrayC = t.Type; -export type DefaultCommentsPartialArrayC = t.Type< - CommentsPartialArray, - CommentsPartialArray, - unknown ->; /** * Types the DefaultCommentsArray as: @@ -26,24 +21,8 @@ export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type< unknown >( 'DefaultCommentsArray', - t.array(comment).is, - (input, context): Either => - input == null ? t.success([]) : t.array(comment).validate(input, context), - t.identity -); - -/** - * Types the DefaultCommentsPartialArray as: - * - If null or undefined, then a default array of type entry will be set - */ -export const DefaultCommentsPartialArray: DefaultCommentsPartialArrayC = new t.Type< - CommentsPartialArray, - CommentsPartialArray, - unknown ->( - 'DefaultCommentsPartialArray', - t.array(commentPartial).is, - (input, context): Either => - input == null ? t.success([]) : t.array(commentPartial).validate(input, context), + t.array(comments).is, + (input): Either => + input == null ? t.success([]) : t.array(comments).decode(input), t.identity ); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts new file mode 100644 index 000000000000..f5ef7d0ad96b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DefaultCreateCommentsArray } from './default_create_comments_array'; +import { CreateCommentsArray } from './create_comments'; +import { getCreateCommentsArrayMock } from './create_comments.mock'; + +describe('default_create_comments_array', () => { + test('it should validate an empty array', () => { + const payload: CreateCommentsArray = []; + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of comments', () => { + const payload: CreateCommentsArray = getCreateCommentsArrayMock(); + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + // TODO: Known weird error formatting that is on our list to address + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<{| comment: string |}>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts new file mode 100644 index 000000000000..51431b9c3985 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts @@ -0,0 +1,28 @@ +/* + * 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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { CreateCommentsArray, createComments } from './create_comments'; + +export type DefaultCreateCommentsArrayC = t.Type; + +/** + * Types the DefaultCreateComments as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCreateCommentsArray: DefaultCreateCommentsArrayC = new t.Type< + CreateCommentsArray, + CreateCommentsArray, + unknown +>( + 'DefaultCreateComments', + t.array(createComments).is, + (input): Either => + input == null ? t.success([]) : t.array(createComments).decode(input), + t.identity +); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts new file mode 100644 index 000000000000..b023e73cb932 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DefaultUpdateCommentsArray } from './default_update_comments_array'; +import { UpdateCommentsArray } from './update_comments'; +import { getUpdateCommentsArrayMock } from './update_comments.mock'; + +describe('default_update_comments_array', () => { + test('it should validate an empty array', () => { + const payload: UpdateCommentsArray = []; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of comments', () => { + const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an array of numbers', () => { + const payload = [1]; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + // TODO: Known weird error formatting that is on our list to address + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts new file mode 100644 index 000000000000..c2593826a635 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts @@ -0,0 +1,28 @@ +/* + * 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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { UpdateCommentsArray, updateCommentsArray } from './update_comments'; + +export type DefaultUpdateCommentsArrayC = t.Type; + +/** + * Types the DefaultCommentsUpdate as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultUpdateCommentsArray: DefaultUpdateCommentsArrayC = new t.Type< + UpdateCommentsArray, + UpdateCommentsArray, + unknown +>( + 'DefaultCreateComments', + updateCommentsArray.is, + (input): Either => + input == null ? t.success([]) : updateCommentsArray.decode(input), + t.identity +); diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 97f2b0f59a5f..16433e00f2b1 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -3,8 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export * from './default_comments_array'; -export * from './default_entries_array'; -export * from './default_namespace'; export * from './comments'; +export * from './create_comments'; +export * from './update_comments'; +export * from './default_comments_array'; +export * from './default_create_comments_array'; +export * from './default_update_comments_array'; +export * from './default_namespace'; +export * from './default_entries_array'; export * from './entries'; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts new file mode 100644 index 000000000000..3e963c2607dc --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts @@ -0,0 +1,14 @@ +/* + * 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 { getCommentsMock } from './comments.mock'; +import { getCreateCommentsMock } from './create_comments.mock'; +import { UpdateCommentsArray } from './update_comments'; + +export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ + getCommentsMock(), + getCreateCommentsMock(), +]; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts new file mode 100644 index 000000000000..7668504b031b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getUpdateCommentsArrayMock } from './update_comments.mock'; +import { + UpdateCommentsArray, + UpdateCommentsArrayOrUndefined, + updateCommentsArray, + updateCommentsArrayOrUndefined, +} from './update_comments'; +import { getCommentsMock } from './comments.mock'; +import { getCreateCommentsMock } from './create_comments.mock'; + +describe('CommentsUpdate', () => { + describe('updateCommentsArray', () => { + test('it should validate an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of existing comments', () => { + const payload = [getCommentsMock()]; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of new comments', () => { + const payload = [getCreateCommentsMock()]; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArray; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('updateCommentsArrayOrUndefined', () => { + test('it should validate an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.ts new file mode 100644 index 000000000000..4a21bfa363d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.ts @@ -0,0 +1,14 @@ +/* + * 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 * as t from 'io-ts'; + +import { comments } from './comments'; +import { createComments } from './create_comments'; + +export const updateCommentsArray = t.array(t.union([comments, createComments])); +export type UpdateCommentsArray = t.TypeOf; +export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); +export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 72a689650ea2..975641b9bebe 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -250,7 +250,7 @@ describe('Exceptions Lists API', () => { }); // TODO Would like to just use getExceptionListSchemaMock() here, but // validation returns object in different order, making the strings not match - expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 2ab7695d8c17..a581cfd08ecc 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -176,7 +176,7 @@ export const updateExceptionListItem = async ({ if (validatedRequest != null) { try { - const response = await http.fetch(EXCEPTION_LIST_URL, { + const response = await http.fetch(EXCEPTION_LIST_ITEM_URL, { body: JSON.stringify(listItem), method: 'PUT', signal, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 57bc63e6f7e3..fc04c5e278d6 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -77,6 +77,12 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = { created_by: { type: 'keyword', }, + updated_at: { + type: 'keyword', + }, + updated_by: { + type: 'keyword', + }, }, }, entries: { diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json index 33c9303c7b52..08bd95b7d124 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json @@ -5,14 +5,7 @@ "type": "simple", "description": "This is a sample change here this list", "name": "Sample Endpoint Exception List update change", - "comments": [ - { - "comment": "this was an old comment.", - "created_by": "lily", - "created_at": "2020-04-20T15:25:31.830Z" - }, - { "comment": "this is a newly added comment" } - ], + "comments": [{ "comment": "this is a newly added comment" }], "entries": [ { "field": "event.category", diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 22a9fbcfb53a..a84283aeabbb 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; import { - CommentsPartialArray, + CreateCommentsArray, Description, EntriesArray, ExceptionListItemSchema, @@ -25,13 +25,13 @@ import { import { getSavedObjectType, - transformComments, + transformCreateCommentsToComments, transformSavedObjectToExceptionListItem, } from './utils'; interface CreateExceptionListItemOptions { _tags: _Tags; - comments: CommentsPartialArray; + comments: CreateCommentsArray; listId: ListId; itemId: ItemId; savedObjectsClient: SavedObjectsClientContract; @@ -64,9 +64,10 @@ export const createExceptionListItem = async ({ }: CreateExceptionListItemOptions): Promise => { const savedObjectType = getSavedObjectType({ namespaceType }); const dateNow = new Date().toISOString(); + const transformedComments = transformCreateCommentsToComments({ comments, user }); const savedObject = await savedObjectsClient.create(savedObjectType, { _tags, - comments: transformComments({ comments, user }), + comments: transformedComments, created_at: dateNow, created_by: user, description, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 03f5de516561..203d32911a6d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - CommentsPartialArray, + CreateCommentsArray, Description, DescriptionOrUndefined, EntriesArray, @@ -30,6 +30,7 @@ import { SortOrderOrUndefined, Tags, TagsOrUndefined, + UpdateCommentsArray, _Tags, _TagsOrUndefined, } from '../../../common/schemas'; @@ -88,7 +89,7 @@ export interface GetExceptionListItemOptions { export interface CreateExceptionListItemOptions { _tags: _Tags; - comments: CommentsPartialArray; + comments: CreateCommentsArray; entries: EntriesArray; itemId: ItemId; listId: ListId; @@ -102,7 +103,7 @@ export interface CreateExceptionListItemOptions { export interface UpdateExceptionListItemOptions { _tags: _TagsOrUndefined; - comments: CommentsPartialArray; + comments: UpdateCommentsArray; entries: EntriesArrayOrUndefined; id: IdOrUndefined; itemId: ItemIdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 7ca9bfd83ab6..5578063fd9b6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -7,7 +7,6 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - CommentsPartialArray, DescriptionOrUndefined, EntriesArrayOrUndefined, ExceptionListItemSchema, @@ -19,19 +18,20 @@ import { NameOrUndefined, NamespaceType, TagsOrUndefined, + UpdateCommentsArrayOrUndefined, _TagsOrUndefined, } from '../../../common/schemas'; import { getSavedObjectType, - transformComments, transformSavedObjectUpdateToExceptionListItem, + transformUpdateCommentsToComments, } from './utils'; import { getExceptionListItem } from './get_exception_list_item'; interface UpdateExceptionListItemOptions { id: IdOrUndefined; - comments: CommentsPartialArray; + comments: UpdateCommentsArrayOrUndefined; _tags: _TagsOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; @@ -71,12 +71,17 @@ export const updateExceptionListItem = async ({ if (exceptionListItem == null) { return null; } else { + const transformedComments = transformUpdateCommentsToComments({ + comments, + existingComments: exceptionListItem.comments, + user, + }); const savedObject = await savedObjectsClient.update( savedObjectType, exceptionListItem.id, { _tags, - comments: transformComments({ comments, user }), + comments: transformedComments, description, entries, meta, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts new file mode 100644 index 000000000000..9cc2aacd8845 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -0,0 +1,437 @@ +/* + * 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 sinon from 'sinon'; +import moment from 'moment'; + +import { DATE_NOW, USER } from '../../../common/constants.mock'; + +import { + isCommentEqual, + transformCreateCommentsToComments, + transformUpdateComments, + transformUpdateCommentsToComments, +} from './utils'; + +describe('utils', () => { + const anchor = '2020-06-17T20:34:51.337Z'; + const unix = moment(anchor).valueOf(); + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('#transformUpdateCommentsToComments', () => { + test('it returns empty array if "comments" is undefined and no comments exist', () => { + const comments = transformUpdateCommentsToComments({ + comments: undefined, + existingComments: [], + user: 'lily', + }); + + expect(comments).toEqual([]); + }); + + test('it formats newly added comments', () => { + const comments = transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: anchor, + created_by: 'lily', + }, + { + comment: 'Im a new comment', + created_at: anchor, + created_by: 'lily', + }, + ]); + }); + + test('it formats multiple newly added comments', () => { + const comments = transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + { comment: 'Im another new comment' }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: anchor, + created_by: 'lily', + }, + { + comment: 'Im a new comment', + created_at: anchor, + created_by: 'lily', + }, + { + comment: 'Im another new comment', + created_at: anchor, + created_by: 'lily', + }, + ]); + }); + + test('it should not throw if comments match existing comments', () => { + const comments = transformUpdateCommentsToComments({ + comments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: anchor, + created_by: 'lily', + }, + ]); + }); + + test('it does not throw if user tries to update one of their own existing comments', () => { + const comments = transformUpdateCommentsToComments({ + comments: [ + { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + updated_at: anchor, + updated_by: 'lily', + }, + ]); + }); + + test('it throws an error if user tries to update their comment, without passing in the "created_at" and "created_by" properties', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [ + { + comment: 'Im an old comment that is trying to be updated', + }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"` + ); + }); + + test('it throws an error if user tries to delete comments', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Comments cannot be deleted, only new comments may be added"` + ); + }); + + test('it throws if user tries to update existing comment timestamp', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); + }); + + test('it throws if user tries to update existing comment author', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' }, + ], + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); + }); + + test('it throws if user tries to update an existing comment that is not their own', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [ + { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); + }); + + test('it throws if user tries to update order of comments', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im a new comment' }, + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"` + ); + }); + + test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + ], + existingComments: [], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`); + }); + + test('it throws if empty comment exists', () => { + expect(() => + transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: ' ' }, + ], + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`); + }); + }); + + describe('#transformCreateCommentsToComments', () => { + test('it returns "undefined" if "comments" is "undefined"', () => { + const comments = transformCreateCommentsToComments({ + comments: undefined, + user: 'lily', + }); + + expect(comments).toBeUndefined(); + }); + + test('it formats newly added comments', () => { + const comments = transformCreateCommentsToComments({ + comments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im a new comment', + created_at: anchor, + created_by: 'lily', + }, + { + comment: 'Im another new comment', + created_at: anchor, + created_by: 'lily', + }, + ]); + }); + + test('it throws an error if user tries to add an empty comment', () => { + expect(() => + transformCreateCommentsToComments({ + comments: [{ comment: ' ' }], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`); + }); + }); + + describe('#transformUpdateComments', () => { + test('it updates comment and adds "updated_at" and "updated_by"', () => { + const comments = transformUpdateComments({ + comment: { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + }, + existingComment: { + comment: 'Im an old comment', + created_at: DATE_NOW, + created_by: 'lily', + }, + user: 'lily', + }); + + expect(comments).toEqual({ + comment: 'Im an old comment that is trying to be updated', + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'lily', + updated_at: anchor, + updated_by: 'lily', + }); + }); + + test('it throws if user tries to update an existing comment that is not their own', () => { + expect(() => + transformUpdateComments({ + comment: { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + }, + existingComment: { + comment: 'Im an old comment', + created_at: DATE_NOW, + created_by: 'lily', + }, + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); + }); + + test('it throws if user tries to update an existing comments timestamp', () => { + expect(() => + transformUpdateComments({ + comment: { + comment: 'Im an old comment that is trying to be updated', + created_at: anchor, + created_by: 'lily', + }, + existingComment: { + comment: 'Im an old comment', + created_at: DATE_NOW, + created_by: 'lily', + }, + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Unable to update comment"`); + }); + }); + + describe('#isCommentEqual', () => { + test('it returns false if "comment" values differ', () => { + const result = isCommentEqual( + { + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + }, + { + comment: 'some older comment', + created_at: DATE_NOW, + created_by: USER, + } + ); + + expect(result).toBeFalsy(); + }); + + test('it returns false if "created_at" values differ', () => { + const result = isCommentEqual( + { + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + }, + { + comment: 'some old comment', + created_at: anchor, + created_by: USER, + } + ); + + expect(result).toBeFalsy(); + }); + + test('it returns false if "created_by" values differ', () => { + const result = isCommentEqual( + { + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + }, + { + comment: 'some old comment', + created_at: DATE_NOW, + created_by: 'lily', + } + ); + + expect(result).toBeFalsy(); + }); + + test('it returns true if comment values are equivalent', () => { + const result = isCommentEqual( + { + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + }, + { + created_at: DATE_NOW, + created_by: USER, + // Disabling to assure that order doesn't matter + // eslint-disable-next-line sort-keys + comment: 'some old comment', + } + ); + + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 5690a42bed87..14b5309f67dc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -6,15 +6,21 @@ import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; import { + Comments, + CommentsArray, CommentsArrayOrUndefined, - CommentsPartialArrayOrUndefined, + CreateComments, + CreateCommentsArrayOrUndefined, ExceptionListItemSchema, ExceptionListSchema, ExceptionListSoSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, NamespaceType, + UpdateCommentsArrayOrUndefined, + comments as commentsSchema, } from '../../../common/schemas'; import { SavedObjectType, @@ -251,21 +257,103 @@ export const transformSavedObjectsToFoundExceptionList = ({ }; }; -export const transformComments = ({ +/* + * Determines whether two comments are equal, this is a very + * naive implementation, not meant to be used for deep equality of complex objects + */ +export const isCommentEqual = (commentA: Comments, commentB: Comments): boolean => { + const a = Object.values(commentA).sort().join(); + const b = Object.values(commentB).sort().join(); + + return a === b; +}; + +export const transformUpdateCommentsToComments = ({ + comments, + existingComments, + user, +}: { + comments: UpdateCommentsArrayOrUndefined; + existingComments: CommentsArray; + user: string; +}): CommentsArray => { + const newComments = comments ?? []; + + if (newComments.length < existingComments.length) { + throw new ErrorWithStatusCode( + 'Comments cannot be deleted, only new comments may be added', + 403 + ); + } else { + return newComments.flatMap((c, index) => { + const existingComment = existingComments[index]; + + if (commentsSchema.is(existingComment) && !commentsSchema.is(c)) { + throw new ErrorWithStatusCode( + 'When trying to update a comment, "created_at" and "created_by" must be present', + 403 + ); + } else if (commentsSchema.is(c) && existingComment == null) { + throw new ErrorWithStatusCode('Only new comments may be added', 403); + } else if ( + commentsSchema.is(c) && + existingComment != null && + !isCommentEqual(c, existingComment) + ) { + return transformUpdateComments({ comment: c, existingComment, user }); + } else { + return transformCreateCommentsToComments({ comments: [c], user }) ?? []; + } + }); + } +}; + +export const transformUpdateComments = ({ + comment, + existingComment, + user, +}: { + comment: Comments; + existingComment: Comments; + user: string; +}): Comments => { + if (comment.created_by !== user) { + // existing comment is being edited, can only be edited by author + throw new ErrorWithStatusCode('Not authorized to edit others comments', 401); + } else if (existingComment.created_at !== comment.created_at) { + throw new ErrorWithStatusCode('Unable to update comment', 403); + } else if (comment.comment.trim().length === 0) { + throw new ErrorWithStatusCode('Empty comments not allowed', 403); + } else { + const dateNow = new Date().toISOString(); + + return { + ...comment, + updated_at: dateNow, + updated_by: user, + }; + } +}; + +export const transformCreateCommentsToComments = ({ comments, user, }: { - comments: CommentsPartialArrayOrUndefined; + comments: CreateCommentsArrayOrUndefined; user: string; }): CommentsArrayOrUndefined => { const dateNow = new Date().toISOString(); if (comments != null) { - return comments.map((comment) => { - return { - comment: comment.comment, - created_at: comment.created_at ?? dateNow, - created_by: comment.created_by ?? user, - }; + return comments.map((c: CreateComments) => { + if (c.comment.trim().length === 0) { + throw new ErrorWithStatusCode('Empty comments not allowed', 403); + } else { + return { + comment: c.comment, + created_at: dateNow, + created_by: user, + }; + } }); } else { return comments; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 244819080c93..b936aea04769 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -37,7 +37,7 @@ import { getEntryMatchAnyMock, getEntriesArrayMock, } from '../../../../../lists/common/schemas/types/entries.mock'; -import { getCommentsMock } from '../../../../../lists/common/schemas/types/comments.mock'; +import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock'; describe('Exception helpers', () => { beforeEach(() => { @@ -382,7 +382,7 @@ describe('Exception helpers', () => { describe('#getFormattedComments', () => { test('it returns formatted comment object with username and timestamp', () => { - const payload = getCommentsMock(); + const payload = getCommentsArrayMock(); const result = getFormattedComments(payload); expect(result[0].username).toEqual('some user'); @@ -390,7 +390,7 @@ describe('Exception helpers', () => { }); test('it returns formatted timeline icon with comment users initial', () => { - const payload = getCommentsMock(); + const payload = getCommentsArrayMock(); const result = getFormattedComments(payload); const wrapper = mount(result[0].timelineIcon as React.ReactElement); @@ -399,12 +399,12 @@ describe('Exception helpers', () => { }); test('it returns comment text', () => { - const payload = getCommentsMock(); + const payload = getCommentsArrayMock(); const result = getFormattedComments(payload); const wrapper = mount(result[0].children as React.ReactElement); - expect(wrapper.text()).toEqual('some comment'); + expect(wrapper.text()).toEqual('some old comment'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 164940db619f..ae4131f9f62c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -10,9 +10,10 @@ import { capitalize } from 'lodash'; import moment from 'moment'; import * as i18n from './translations'; -import { FormattedEntry, OperatorOption, DescriptionListItem, Comment } from './types'; +import { FormattedEntry, OperatorOption, DescriptionListItem } from './types'; import { EXCEPTION_OPERATORS, isOperator } from './operators'; import { + CommentsArray, Entry, EntriesArray, ExceptionListItemSchema, @@ -183,7 +184,7 @@ export const getDescriptionListContent = ( * * @param comments ExceptionItem.comments */ -export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] => +export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] => comments.map((comment) => ({ username: comment.created_by, timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 24c328462ce2..ed2be64b4430 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -26,12 +26,6 @@ export interface DescriptionListItem { description: NonNullable; } -export interface Comment { - created_by: string; - created_at: string; - comment: string; -} - export enum ExceptionListType { DETECTION_ENGINE = 'detection', ENDPOINT = 'endpoint', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index 3ea8507d82a1..f5b34b7838d2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -12,7 +12,7 @@ import moment from 'moment-timezone'; import { ExceptionDetails } from './exception_details'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; describe('ExceptionDetails', () => { beforeEach(() => { @@ -42,7 +42,7 @@ describe('ExceptionDetails', () => { test('it renders comments button if comments exist', () => { const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { test('it renders correct number of comments', () => { const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = [getCommentsMock()[0]]; + exceptionItem.comments = [getCommentsArrayMock()[0]]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { test('it renders comments plural if more than one', () => { const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { test('it renders comments show text if "showComments" is false', () => { const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { test('it renders comments hide text if "showComments" is true', () => { const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { test('it invokes "onCommentsClick" when comments button clicked', () => { const mockOnCommentsClick = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} @@ -68,7 +68,7 @@ storiesOf('Components|ExceptionItem', module) const payload = getExceptionListItemSchemaMock(); payload._tags = []; payload.description = ''; - payload.comments = getCommentsMock(); + payload.comments = getCommentsArrayMock(); payload.entries = [ { field: 'actingProcess.file.signer', @@ -106,7 +106,7 @@ storiesOf('Components|ExceptionItem', module) }) .add('with everything', () => { const payload = getExceptionListItemSchemaMock(); - payload.comments = getCommentsMock(); + payload.comments = getCommentsArrayMock(); return ( { it('it renders ExceptionDetails and ExceptionEntries', () => { @@ -83,7 +83,7 @@ describe('ExceptionItem', () => { it('it renders comment accordion closed to begin with', () => { const mockOnDeleteException = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { it('it renders comment accordion open when showComments is true', () => { const mockOnDeleteException = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsMock(); + exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}>