From 4b98b69ed03df24254ce8c679bf4403a98c604b5 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 17 Jun 2020 22:23:44 -0400 Subject: [PATCH 1/9] updated comments logic to ensure that they are append only --- .../create_exception_list_item_schema.test.ts | 15 + .../create_exception_list_item_schema.ts | 6 +- .../update_exception_list_item_schema.ts | 8 +- .../common/schemas/types/comments.mock.ts | 17 ++ .../common/schemas/types/comments.test.ts | 215 ++++++++++++++ .../lists/common/schemas/types/comments.ts | 32 +- .../common/schemas/types/comments_new.mock.ts | 15 + .../common/schemas/types/comments_new.test.ts | 126 ++++++++ .../common/schemas/types/comments_new.ts | 18 ++ .../schemas/types/comments_update.mock.ts | 14 + .../schemas/types/comments_update.test.ts | 106 +++++++ .../common/schemas/types/comments_update.ts | 14 + .../types/default_comments_array.test.ts | 68 +++++ .../schemas/types/default_comments_array.ts | 27 +- .../types/default_comments_new_array.test.ts | 62 ++++ .../types/default_comments_new_array.ts | 28 ++ .../default_comments_update_array.test.ts | 70 +++++ .../types/default_comments_update_array.ts | 28 ++ .../lists/common/schemas/types/index.ts | 4 + .../create_exception_list_item.ts | 8 +- .../update_exception_list_item.ts | 12 +- .../services/exception_lists/utils.test.ts | 275 ++++++++++++++++++ .../server/services/exception_lists/utils.ts | 84 +++++- 23 files changed, 1181 insertions(+), 71 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/types/comments.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_new.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_new.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_update.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/comments_update.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_new_array.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_update_array.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_update_array.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/utils.test.ts 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 61437b1f04ce3..0d6714b96243e 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,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { CommentsArray } from '../types'; import { CreateExceptionListItemSchema, @@ -101,6 +102,20 @@ describe('create_exception_list_schema', () => { expect(message.schema).toEqual(outputPayload); }); + test('it should NOT accept "comments" with "created_at" or "created_by" values', () => { + const inputPayload: Omit & { + comments?: CommentsArray; + } = { + ...getCreateExceptionListItemSchemaMock(), + comments: [{ comment: 'some comment', created_at: 'some time', created_by: 'someone' }], + }; + 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 accept an undefined for "entries" but return an array', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); 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 f593b5d164035..a88970356c7c3 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 { CommentsNewArray, DefaultCommentsNewArray, 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: DefaultCommentsNewArray, // 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: CommentsNewArray; 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 c32b15fecb571..44c3279ce19e1 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,8 +23,8 @@ import { } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; import { - CommentsPartialArray, - DefaultCommentsPartialArray, + CommentsUpdateArray, + DefaultCommentsUpdateArray, DefaultEntryArray, EntriesArray, } from '../types'; @@ -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: DefaultCommentsUpdateArray, // 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: CommentsUpdateArray; 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 new file mode 100644 index 0000000000000..9e56ac292f8b5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments.mock.ts @@ -0,0 +1,17 @@ +/* + * 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 { DATE_NOW, USER } from '../../constants.mock'; + +import { Comments, CommentsArray } from './comments'; + +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 0000000000000..e737fd62e0d62 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments.test.ts @@ -0,0 +1,215 @@ +/* + * 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 ""', + 'Invalid value "undefined" supplied to ""', + ]); + 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 ""']); + 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 ""', + 'Invalid value "1" supplied to ""', + ]); + 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 ""', + 'Invalid value "1" supplied to ""', + ]); + 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 d61608c3508f4..0ee3b05c8102f 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, + created_at: t.string, // TODO: Make this into an ISO Date string check, + created_by: t.string, }) ), t.exact( t.partial({ - created_at: t.string, // TODO: Make this into an ISO Date string check, - created_by: t.string, + 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/comments_new.mock.ts b/x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts new file mode 100644 index 0000000000000..916c74eedcadf --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts @@ -0,0 +1,15 @@ +/* + * 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 { CommentsNew, CommentsNewArray } from './comments_new'; + +export const getCommentsNewMock = (): CommentsNew => ({ + comment: 'some comments', +}); + +export const getCommentsNewArrayMock = (): CommentsNewArray => [ + getCommentsNewMock(), + getCommentsNewMock(), +]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_new.test.ts b/x-pack/plugins/lists/common/schemas/types/comments_new.test.ts new file mode 100644 index 0000000000000..6c9fbb94eb8ab --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_new.test.ts @@ -0,0 +1,126 @@ +/* + * 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 { getCommentsNewArrayMock, getCommentsNewMock } from './comments_new.mock'; +import { + CommentsNew, + CommentsNewArray, + CommentsNewArrayOrUndefined, + commentsNew, + commentsNewArray, + commentsNewArrayOrUndefined, +} from './comments_new'; + +describe('CommentsNew', () => { + describe('commentsNew', () => { + test('it should validate a comments', () => { + const payload = getCommentsNewMock(); + const decoded = commentsNew.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 = commentsNew.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCommentsNewMock(), + comment: ['some value'], + }; + const decoded = commentsNew.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: CommentsNew & { + extraKey?: string; + } = getCommentsNewMock(); + payload.extraKey = 'some value'; + const decoded = commentsNew.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCommentsNewMock()); + }); + }); + + describe('commentsNewArray', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsNewArrayMock(); + const decoded = commentsNewArray.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 = commentsNewArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CommentsNewArray; + const decoded = commentsNewArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + }); + + describe('commentsNewArrayOrUndefined', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsNewArrayMock(); + const decoded = commentsNewArrayOrUndefined.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 = commentsNewArrayOrUndefined.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 CommentsNewArrayOrUndefined; + const decoded = commentsNewArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/comments_new.ts b/x-pack/plugins/lists/common/schemas/types/comments_new.ts new file mode 100644 index 0000000000000..692adcdaff59a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_new.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 commentsNew = t.exact( + t.type({ + comment: t.string, + }) +); + +export const commentsNewArray = t.array(commentsNew); +export type CommentsNewArray = t.TypeOf; +export type CommentsNew = t.TypeOf; +export const commentsNewArrayOrUndefined = t.union([commentsNewArray, t.undefined]); +export type CommentsNewArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts b/x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts new file mode 100644 index 0000000000000..f02a4d0214663 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_update.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 { getCommentsNewMock } from './comments_new.mock'; +import { CommentsUpdateArray } from './comments_update'; + +export const getCommentsUpdateArrayMock = (): CommentsUpdateArray => [ + getCommentsMock(), + getCommentsNewMock(), +]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.test.ts b/x-pack/plugins/lists/common/schemas/types/comments_update.test.ts new file mode 100644 index 0000000000000..747bab90dfb2f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_update.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { getCommentsUpdateArrayMock } from './comments_update.mock'; +import { + CommentsUpdateArray, + CommentsUpdateArrayOrUndefined, + commentsUpdateArray, + commentsUpdateArrayOrUndefined, +} from './comments_update'; +import { getCommentsMock } from './comments.mock'; +import { getCommentsNewMock } from './comments_new.mock'; + +describe('CommentsUpdate', () => { + describe('commentsUpdateArray', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsUpdateArrayMock(); + const decoded = commentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it a', () => { + const payload = [getCommentsMock()]; + const decoded = commentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it b', () => { + const payload = [getCommentsNewMock()]; + const decoded = commentsUpdateArray.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 = commentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate when array includes non comments types', () => { + const payload = ([1] as unknown) as CommentsUpdateArray; + const decoded = commentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "1" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('commentsUpdateArrayOrUndefined', () => { + test('it should validate an array of comments', () => { + const payload = getCommentsUpdateArrayMock(); + const decoded = commentsUpdateArrayOrUndefined.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 = commentsUpdateArrayOrUndefined.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 CommentsUpdateArrayOrUndefined; + const decoded = commentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "1" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.ts b/x-pack/plugins/lists/common/schemas/types/comments_update.ts new file mode 100644 index 0000000000000..1c5b1cf538815 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments_update.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 { commentsNew } from './comments_new'; + +export const commentsUpdateArray = t.array(t.union([comments, commentsNew])); +export type CommentsUpdateArray = t.TypeOf; +export const commentsUpdateArrayOrUndefined = t.union([commentsUpdateArray, t.undefined]); +export type CommentsUpdateArrayOrUndefined = 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 0000000000000..d63f343c35e9c --- /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 ""', + 'Invalid value "1" supplied to ""', + ]); + 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 ""', + 'Invalid value "some string" supplied to ""', + ]); + 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 a80bb968561f0..e8be299246ab8 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, + t.array(comments).is, (input): Either => - input == null ? t.success([]) : t.array(comment).decode(input), - 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): Either => - input == null ? t.success([]) : t.array(commentPartial).decode(input), + input == null ? t.success([]) : t.array(comments).decode(input), t.identity ); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts new file mode 100644 index 0000000000000..4d180a0e9dc2d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { DefaultCommentsNewArray } from './default_comments_new_array'; +import { CommentsNewArray } from './comments_new'; +import { getCommentsNewArrayMock } from './comments_new.mock'; + +describe('default_comments_new_array', () => { + test('it should validate an empty array', () => { + const payload: CommentsNewArray = []; + const decoded = DefaultCommentsNewArray.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: CommentsNewArray = getCommentsNewArrayMock(); + const decoded = DefaultCommentsNewArray.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 = DefaultCommentsNewArray.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 ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsNewArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsNewArray.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_new_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.ts new file mode 100644 index 0000000000000..b6f93a4c8515d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_new_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 { CommentsNewArray, commentsNew } from './comments_new'; + +export type DefaultCommentsNewArrayC = t.Type; + +/** + * Types the DefaultCommentsNew as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCommentsNewArray: DefaultCommentsNewArrayC = new t.Type< + CommentsNewArray, + CommentsNewArray, + unknown +>( + 'DefaultCommentsNew', + t.array(commentsNew).is, + (input): Either => + input == null ? t.success([]) : t.array(commentsNew).decode(input), + t.identity +); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.test.ts new file mode 100644 index 0000000000000..698377ddce3b7 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_update_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 { DefaultCommentsUpdateArray } from './default_comments_update_array'; +import { CommentsUpdateArray } from './comments_update'; +import { getCommentsUpdateArrayMock } from './comments_update.mock'; + +describe('default_comments_update_array', () => { + test('it should validate an empty array', () => { + const payload: CommentsUpdateArray = []; + const decoded = DefaultCommentsUpdateArray.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: CommentsUpdateArray = getCommentsUpdateArrayMock(); + const decoded = DefaultCommentsUpdateArray.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 = DefaultCommentsUpdateArray.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 ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "1" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsUpdateArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to ""', + 'Invalid value "some string" supplied to ""', + 'Invalid value "some string" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsUpdateArray.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_update_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.ts new file mode 100644 index 0000000000000..1b4d28a21321e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_update_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 { CommentsUpdateArray, commentsUpdateArray } from './comments_update'; + +export type DefaultCommentsUpdateArrayC = t.Type; + +/** + * Types the DefaultCommentsUpdate as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCommentsUpdateArray: DefaultCommentsUpdateArrayC = new t.Type< + CommentsUpdateArray, + CommentsUpdateArray, + unknown +>( + 'DefaultCreateComments', + commentsUpdateArray.is, + (input): Either => + input == null ? t.success([]) : commentsUpdateArray.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 8e4b28b31d95c..f1a0385f075b2 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export * from './default_comments_array'; +export * from './default_comments_new_array'; +export * from './default_comments_update_array'; export * from './default_entries_array'; export * from './comments'; +export * from './comments_new'; +export * from './comments_update'; export * from './entries'; 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 22a9fbcfb53af..48f21f5a1c7f6 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, + CommentsNewArray, Description, EntriesArray, ExceptionListItemSchema, @@ -25,13 +25,13 @@ import { import { getSavedObjectType, - transformComments, + transformNewComments, transformSavedObjectToExceptionListItem, } from './utils'; interface CreateExceptionListItemOptions { _tags: _Tags; - comments: CommentsPartialArray; + comments: CommentsNewArray; listId: ListId; itemId: ItemId; savedObjectsClient: SavedObjectsClientContract; @@ -66,7 +66,7 @@ export const createExceptionListItem = async ({ const dateNow = new Date().toISOString(); const savedObject = await savedObjectsClient.create(savedObjectType, { _tags, - comments: transformComments({ comments, user }), + comments: transformNewComments({ newComments: comments, user }), created_at: dateNow, created_by: user, description, 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 7ca9bfd83ab64..a6896117744a6 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,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - CommentsPartialArray, + CommentsUpdateArrayOrUndefined, DescriptionOrUndefined, EntriesArrayOrUndefined, ExceptionListItemSchema, @@ -24,14 +24,14 @@ import { import { getSavedObjectType, - transformComments, + transformCommentsUpdate, transformSavedObjectUpdateToExceptionListItem, } from './utils'; import { getExceptionListItem } from './get_exception_list_item'; interface UpdateExceptionListItemOptions { id: IdOrUndefined; - comments: CommentsPartialArray; + comments: CommentsUpdateArrayOrUndefined; _tags: _TagsOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; @@ -76,7 +76,11 @@ export const updateExceptionListItem = async ({ exceptionListItem.id, { _tags, - comments: transformComments({ comments, user }), + comments: transformCommentsUpdate({ + existingComments: exceptionListItem.comments, + newComments: comments, + user, + }), 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 0000000000000..d458a5934fd05 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import sinon from 'sinon'; +import moment from 'moment'; + +import { DATE_NOW, USER } from '../../../common/constants.mock'; + +import { isCommentEqual, transformCommentsUpdate, transformNewComments } 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('#transformCommentsUpdate', () => { + test('it formats newly appended comments', () => { + const comments = transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'lily', + }, + { + comment: 'Im a new comment', + created_at: '2020-06-17T20:34:51.337Z', + created_by: 'lily', + }, + ]); + }); + + test('it formats multiple newly appended comments', () => { + const comments = transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + { comment: 'Im another new comment' }, + ], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'lily', + }, + { + comment: 'Im a new comment', + created_at: '2020-06-17T20:34:51.337Z', + created_by: 'lily', + }, + { + comment: 'Im another new comment', + created_at: '2020-06-17T20:34:51.337Z', + created_by: 'lily', + }, + ]); + }); + + test('it should not throw if comments match existing comments', () => { + const comments = transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im an old comment', + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'lily', + }, + ]); + }); + + test('it throws if user tries to update existing comment timestamp', () => { + expect(() => + transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Existing comments cannot be edited, only new comments may be appended"` + ); + }); + + test('it throws if user tries to update existing comment author', () => { + expect(() => + transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' }, + ], + newComments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Existing comments cannot be edited, only new comments may be appended"` + ); + }); + + test('it throws if user tries to update existing comment', () => { + expect(() => + transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [ + { + comment: 'Im an old comment that is trying to be updated', + created_at: DATE_NOW, + created_by: 'lily', + }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Existing comments cannot be edited, only new comments may be appended"` + ); + }); + + test('it throws if user tries to update order of comments', () => { + expect(() => + transformCommentsUpdate({ + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + newComments: [ + { comment: 'Im a new comment' }, + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Existing comments cannot be edited, only new comments may be appended"` + ); + }); + + test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => { + expect(() => + transformCommentsUpdate({ + existingComments: [], + newComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im a new comment' }, + ], + user: 'lily', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be appended"`); + }); + }); + + describe('#transformNewComments', () => { + test('it formats newly appended comments', () => { + const comments = transformNewComments({ + newComments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }], + user: 'lily', + }); + + expect(comments).toEqual([ + { + comment: 'Im a new comment', + created_at: '2020-06-17T20:34:51.337Z', + created_by: 'lily', + }, + { + comment: 'Im another new comment', + created_at: '2020-06-17T20:34:51.337Z', + created_by: 'lily', + }, + ]); + }); + }); + + 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 5690a42bed87e..fab278bbae744 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, CommentsArrayOrUndefined, - CommentsPartialArrayOrUndefined, + CommentsNew, + CommentsNewArrayOrUndefined, + CommentsUpdateArrayOrUndefined, ExceptionListItemSchema, ExceptionListSchema, ExceptionListSoSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, NamespaceType, + commentsNew, + comments as commentsSchema, } from '../../../common/schemas'; import { SavedObjectType, @@ -251,23 +257,81 @@ export const transformSavedObjectsToFoundExceptionList = ({ }; }; -export const transformComments = ({ - comments, +/* + * 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 | CommentsNew, + commentB: Comments | CommentsNew +): boolean => { + const a = Object.values(commentA).sort().join(); + const b = Object.values(commentB).sort().join(); + + return a === b; +}; + +export const transformCommentsUpdate = ({ + newComments, + existingComments, + user, +}: { + newComments: CommentsUpdateArrayOrUndefined; + existingComments: CommentsArrayOrUndefined; + user: string; +}): CommentsArrayOrUndefined => { + if ( + newComments != null && + newComments.length === 0 && + existingComments != null && + existingComments.length > 0 + ) { + throw new ErrorWithStatusCode( + 'Comments cannot be deleted, only new comments may be appended', + 403 + ); + } else if (newComments != null) { + const existing = existingComments ?? []; + return newComments.flatMap((c, index) => { + const existingComment = existing[index]; + + // no comments exist, and user tries to add comment of type `comment` + if (commentsSchema.is(c) && existing.length === 0) { + throw new ErrorWithStatusCode('Only new comments may be appended', 403); + } else if (index <= existing.length - 1 && !isCommentEqual(c, existingComment)) { + throw new ErrorWithStatusCode( + 'Existing comments cannot be edited, only new comments may be appended', + 403 + ); + } else if (index > existing.length - 1 && commentsNew.is(c)) { + const newComment = transformNewComments({ newComments: [c], user }); + return newComment ?? []; + } else { + return commentsSchema.is(c) ? c : []; + } + }); + } else { + return existingComments; + } +}; + +export const transformNewComments = ({ + newComments, user, }: { - comments: CommentsPartialArrayOrUndefined; + newComments: CommentsNewArrayOrUndefined; user: string; }): CommentsArrayOrUndefined => { const dateNow = new Date().toISOString(); - if (comments != null) { - return comments.map((comment) => { + if (newComments != null) { + return newComments.map((c: CommentsNew) => { return { - comment: comment.comment, - created_at: comment.created_at ?? dateNow, - created_by: comment.created_by ?? user, + comment: c.comment, + created_at: dateNow, + created_by: user, }; }); } else { - return comments; + return newComments; } }; From bb6e1dc4570861722b66d668dcab3436b668d7a4 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 18 Jun 2020 10:33:11 -0400 Subject: [PATCH 2/9] updated naming and cleaned up logic --- .../create_exception_list_item_schema.ts | 6 +- .../update_exception_list_item_schema.ts | 8 +- .../common/schemas/types/comments_new.ts | 18 -- .../common/schemas/types/comments_update.ts | 14 -- ...ts_new.mock.ts => create_comments.mock.ts} | 10 +- ...ts_new.test.ts => create_comments.test.ts} | 66 +++--- .../common/schemas/types/create_comments.ts | 18 ++ .../types/default_comments_new_array.ts | 28 --- ... => default_create_comments_array.test.ts} | 22 +- .../types/default_create_comments_array.ts | 28 +++ ... => default_update_comments_array.test.ts} | 22 +- ...ay.ts => default_update_comments_array.ts} | 16 +- .../lists/common/schemas/types/index.ts | 10 +- ...update.mock.ts => update_comments.mock.ts} | 8 +- ...update.test.ts => update_comments.test.ts} | 44 ++-- .../common/schemas/types/update_comments.ts | 14 ++ .../create_exception_list_item.ts | 9 +- .../update_exception_list_item.ts | 17 +- .../services/exception_lists/utils.test.ts | 196 +++++++++++++----- .../server/services/exception_lists/utils.ts | 93 +++++---- 20 files changed, 374 insertions(+), 273 deletions(-) delete mode 100644 x-pack/plugins/lists/common/schemas/types/comments_new.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/comments_update.ts rename x-pack/plugins/lists/common/schemas/types/{comments_new.mock.ts => create_comments.mock.ts} (51%) rename x-pack/plugins/lists/common/schemas/types/{comments_new.test.ts => create_comments.test.ts} (66%) create mode 100644 x-pack/plugins/lists/common/schemas/types/create_comments.ts delete mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_new_array.ts rename x-pack/plugins/lists/common/schemas/types/{default_comments_new_array.test.ts => default_create_comments_array.test.ts} (71%) create mode 100644 x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts rename x-pack/plugins/lists/common/schemas/types/{default_comments_update_array.test.ts => default_update_comments_array.test.ts} (74%) rename x-pack/plugins/lists/common/schemas/types/{default_comments_update_array.ts => default_update_comments_array.ts} (54%) rename x-pack/plugins/lists/common/schemas/types/{comments_update.mock.ts => update_comments.mock.ts} (60%) rename x-pack/plugins/lists/common/schemas/types/{comments_update.test.ts => update_comments.test.ts} (70%) create mode 100644 x-pack/plugins/lists/common/schemas/types/update_comments.ts 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 a88970356c7c3..fb452ac89576d 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 { CommentsNewArray, DefaultCommentsNewArray, 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: DefaultCommentsNewArray, // 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: CommentsNewArray; + 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 44c3279ce19e1..582fabdc160f9 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 { - CommentsUpdateArray, - DefaultCommentsUpdateArray, 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: DefaultCommentsUpdateArray, // 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: CommentsUpdateArray; + comments: UpdateCommentsArray; tags: Tags; entries: EntriesArray; namespace_type: NamespaceType; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_new.ts b/x-pack/plugins/lists/common/schemas/types/comments_new.ts deleted file mode 100644 index 692adcdaff59a..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/comments_new.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 commentsNew = t.exact( - t.type({ - comment: t.string, - }) -); - -export const commentsNewArray = t.array(commentsNew); -export type CommentsNewArray = t.TypeOf; -export type CommentsNew = t.TypeOf; -export const commentsNewArrayOrUndefined = t.union([commentsNewArray, t.undefined]); -export type CommentsNewArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.ts b/x-pack/plugins/lists/common/schemas/types/comments_update.ts deleted file mode 100644 index 1c5b1cf538815..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/comments_update.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { commentsNew } from './comments_new'; - -export const commentsUpdateArray = t.array(t.union([comments, commentsNew])); -export type CommentsUpdateArray = t.TypeOf; -export const commentsUpdateArrayOrUndefined = t.union([commentsUpdateArray, t.undefined]); -export type CommentsUpdateArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts similarity index 51% rename from x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts rename to x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts index 916c74eedcadf..7f6731071b9b8 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments_new.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CommentsNew, CommentsNewArray } from './comments_new'; +import { CreateComments, CreateCommentsArray } from './create_comments'; -export const getCommentsNewMock = (): CommentsNew => ({ +export const getCreateCommentsMock = (): CreateComments => ({ comment: 'some comments', }); -export const getCommentsNewArrayMock = (): CommentsNewArray => [ - getCommentsNewMock(), - getCommentsNewMock(), +export const getCreateCommentsArrayMock = (): CreateCommentsArray => [ + getCreateCommentsMock(), + getCreateCommentsMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_new.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts similarity index 66% rename from x-pack/plugins/lists/common/schemas/types/comments_new.test.ts rename to x-pack/plugins/lists/common/schemas/types/create_comments.test.ts index 6c9fbb94eb8ab..794591a109388 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments_new.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts @@ -9,21 +9,21 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCommentsNewArrayMock, getCommentsNewMock } from './comments_new.mock'; +import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comments.mock'; import { - CommentsNew, - CommentsNewArray, - CommentsNewArrayOrUndefined, - commentsNew, - commentsNewArray, - commentsNewArrayOrUndefined, -} from './comments_new'; - -describe('CommentsNew', () => { - describe('commentsNew', () => { + CreateComments, + CreateCommentsArray, + CreateCommentsArrayOrUndefined, + createComments, + createCommentsArray, + createCommentsArrayOrUndefined, +} from './create_comments'; + +describe('CreateComments', () => { + describe('createComments', () => { test('it should validate a comments', () => { - const payload = getCommentsNewMock(); - const decoded = commentsNew.decode(payload); + const payload = getCreateCommentsMock(); + const decoded = createComments.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -32,7 +32,7 @@ describe('CommentsNew', () => { test('it should not validate when undefined', () => { const payload = undefined; - const decoded = commentsNew.decode(payload); + const decoded = createComments.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); @@ -40,11 +40,11 @@ describe('CommentsNew', () => { }); test('it should not validate when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { - ...getCommentsNewMock(), + const payload: Omit & { comment: string[] } = { + ...getCreateCommentsMock(), comment: ['some value'], }; - const decoded = commentsNew.decode(payload); + const decoded = createComments.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -54,22 +54,22 @@ describe('CommentsNew', () => { }); test('it should strip out extra keys', () => { - const payload: CommentsNew & { + const payload: CreateComments & { extraKey?: string; - } = getCommentsNewMock(); + } = getCreateCommentsMock(); payload.extraKey = 'some value'; - const decoded = commentsNew.decode(payload); + const decoded = createComments.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getCommentsNewMock()); + expect(message.schema).toEqual(getCreateCommentsMock()); }); }); - describe('commentsNewArray', () => { + describe('createCommentsArray', () => { test('it should validate an array of comments', () => { - const payload = getCommentsNewArrayMock(); - const decoded = commentsNewArray.decode(payload); + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -78,7 +78,7 @@ describe('CommentsNew', () => { test('it should not validate when undefined', () => { const payload = undefined; - const decoded = commentsNewArray.decode(payload); + const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); @@ -86,8 +86,8 @@ describe('CommentsNew', () => { }); test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as CommentsNewArray; - const decoded = commentsNewArray.decode(payload); + 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 ""']); @@ -95,10 +95,10 @@ describe('CommentsNew', () => { }); }); - describe('commentsNewArrayOrUndefined', () => { + describe('createCommentsArrayOrUndefined', () => { test('it should validate an array of comments', () => { - const payload = getCommentsNewArrayMock(); - const decoded = commentsNewArrayOrUndefined.decode(payload); + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -107,7 +107,7 @@ describe('CommentsNew', () => { test('it should validate when undefined', () => { const payload = undefined; - const decoded = commentsNewArrayOrUndefined.decode(payload); + const decoded = createCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -115,8 +115,8 @@ describe('CommentsNew', () => { }); test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as CommentsNewArrayOrUndefined; - const decoded = commentsNewArray.decode(payload); + 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 ""']); 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 0000000000000..c34419298ef93 --- /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_new_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.ts deleted file mode 100644 index b6f93a4c8515d..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { CommentsNewArray, commentsNew } from './comments_new'; - -export type DefaultCommentsNewArrayC = t.Type; - -/** - * Types the DefaultCommentsNew as: - * - If null or undefined, then a default array of type entry will be set - */ -export const DefaultCommentsNewArray: DefaultCommentsNewArrayC = new t.Type< - CommentsNewArray, - CommentsNewArray, - unknown ->( - 'DefaultCommentsNew', - t.array(commentsNew).is, - (input): Either => - input == null ? t.success([]) : t.array(commentsNew).decode(input), - t.identity -); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts similarity index 71% rename from x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts rename to x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index 4d180a0e9dc2d..1c7b5df282288 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_new_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -9,14 +9,14 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { DefaultCommentsNewArray } from './default_comments_new_array'; -import { CommentsNewArray } from './comments_new'; -import { getCommentsNewArrayMock } from './comments_new.mock'; +import { DefaultCreateCommentsArray } from './default_create_comments_array'; +import { CreateCommentsArray } from './create_comments'; +import { getCreateCommentsArrayMock } from './create_comments.mock'; -describe('default_comments_new_array', () => { +describe('default_create_comments_array', () => { test('it should validate an empty array', () => { - const payload: CommentsNewArray = []; - const decoded = DefaultCommentsNewArray.decode(payload); + const payload: CreateCommentsArray = []; + const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -24,8 +24,8 @@ describe('default_comments_new_array', () => { }); test('it should validate an array of comments', () => { - const payload: CommentsNewArray = getCommentsNewArrayMock(); - const decoded = DefaultCommentsNewArray.decode(payload); + const payload: CreateCommentsArray = getCreateCommentsArrayMock(); + const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -34,7 +34,7 @@ describe('default_comments_new_array', () => { test('it should NOT validate an array of numbers', () => { const payload = [1]; - const decoded = DefaultCommentsNewArray.decode(payload); + const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); // TODO: Known weird error formatting that is on our list to address @@ -44,7 +44,7 @@ describe('default_comments_new_array', () => { test('it should NOT validate an array of strings', () => { const payload = ['some string']; - const decoded = DefaultCommentsNewArray.decode(payload); + const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); @@ -53,7 +53,7 @@ describe('default_comments_new_array', () => { test('it should return a default array entry', () => { const payload = null; - const decoded = DefaultCommentsNewArray.decode(payload); + const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).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 0000000000000..51431b9c39850 --- /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_comments_update_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts similarity index 74% rename from x-pack/plugins/lists/common/schemas/types/default_comments_update_array.test.ts rename to x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 698377ddce3b7..e8262a4478b42 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -9,14 +9,14 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { DefaultCommentsUpdateArray } from './default_comments_update_array'; -import { CommentsUpdateArray } from './comments_update'; -import { getCommentsUpdateArrayMock } from './comments_update.mock'; +import { DefaultUpdateCommentsArray } from './default_update_comments_array'; +import { UpdateCommentsArray } from './update_comments'; +import { getUpdateCommentsArrayMock } from './update_comments.mock'; -describe('default_comments_update_array', () => { +describe('default_update_comments_array', () => { test('it should validate an empty array', () => { - const payload: CommentsUpdateArray = []; - const decoded = DefaultCommentsUpdateArray.decode(payload); + const payload: UpdateCommentsArray = []; + const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -24,8 +24,8 @@ describe('default_comments_update_array', () => { }); test('it should validate an array of comments', () => { - const payload: CommentsUpdateArray = getCommentsUpdateArrayMock(); - const decoded = DefaultCommentsUpdateArray.decode(payload); + const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); + const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -34,7 +34,7 @@ describe('default_comments_update_array', () => { test('it should NOT validate an array of numbers', () => { const payload = [1]; - const decoded = DefaultCommentsUpdateArray.decode(payload); + const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); // TODO: Known weird error formatting that is on our list to address @@ -48,7 +48,7 @@ describe('default_comments_update_array', () => { test('it should NOT validate an array of strings', () => { const payload = ['some string']; - const decoded = DefaultCommentsUpdateArray.decode(payload); + const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -61,7 +61,7 @@ describe('default_comments_update_array', () => { test('it should return a default array entry', () => { const payload = null; - const decoded = DefaultCommentsUpdateArray.decode(payload); + const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts similarity index 54% rename from x-pack/plugins/lists/common/schemas/types/default_comments_update_array.ts rename to x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts index 1b4d28a21321e..c2593826a6358 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_update_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts @@ -7,22 +7,22 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { CommentsUpdateArray, commentsUpdateArray } from './comments_update'; +import { UpdateCommentsArray, updateCommentsArray } from './update_comments'; -export type DefaultCommentsUpdateArrayC = t.Type; +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 DefaultCommentsUpdateArray: DefaultCommentsUpdateArrayC = new t.Type< - CommentsUpdateArray, - CommentsUpdateArray, +export const DefaultUpdateCommentsArray: DefaultUpdateCommentsArrayC = new t.Type< + UpdateCommentsArray, + UpdateCommentsArray, unknown >( 'DefaultCreateComments', - commentsUpdateArray.is, - (input): Either => - input == null ? t.success([]) : commentsUpdateArray.decode(input), + 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 f1a0385f075b2..b231156184499 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -3,11 +3,11 @@ * 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 './comments'; +export * from './create_comments'; export * from './default_comments_array'; -export * from './default_comments_new_array'; -export * from './default_comments_update_array'; +export * from './default_create_comments_array'; export * from './default_entries_array'; -export * from './comments'; -export * from './comments_new'; -export * from './comments_update'; +export * from './default_update_comments_array'; export * from './entries'; +export * from './update_comments'; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts similarity index 60% rename from x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts rename to x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts index f02a4d0214663..3e963c2607dc5 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments_update.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts @@ -5,10 +5,10 @@ */ import { getCommentsMock } from './comments.mock'; -import { getCommentsNewMock } from './comments_new.mock'; -import { CommentsUpdateArray } from './comments_update'; +import { getCreateCommentsMock } from './create_comments.mock'; +import { UpdateCommentsArray } from './update_comments'; -export const getCommentsUpdateArrayMock = (): CommentsUpdateArray => [ +export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ getCommentsMock(), - getCommentsNewMock(), + getCreateCommentsMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments_update.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts similarity index 70% rename from x-pack/plugins/lists/common/schemas/types/comments_update.test.ts rename to x-pack/plugins/lists/common/schemas/types/update_comments.test.ts index 747bab90dfb2f..a5cc5bdbfa8fb 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments_update.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts @@ -9,21 +9,21 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCommentsUpdateArrayMock } from './comments_update.mock'; +import { getUpdateCommentsArrayMock } from './update_comments.mock'; import { - CommentsUpdateArray, - CommentsUpdateArrayOrUndefined, - commentsUpdateArray, - commentsUpdateArrayOrUndefined, -} from './comments_update'; + UpdateCommentsArray, + UpdateCommentsArrayOrUndefined, + updateCommentsArray, + updateCommentsArrayOrUndefined, +} from './update_comments'; import { getCommentsMock } from './comments.mock'; -import { getCommentsNewMock } from './comments_new.mock'; +import { getCreateCommentsMock } from './create_comments.mock'; describe('CommentsUpdate', () => { - describe('commentsUpdateArray', () => { + describe('updateCommentsArray', () => { test('it should validate an array of comments', () => { - const payload = getCommentsUpdateArrayMock(); - const decoded = commentsUpdateArray.decode(payload); + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -32,7 +32,7 @@ describe('CommentsUpdate', () => { test('it a', () => { const payload = [getCommentsMock()]; - const decoded = commentsUpdateArray.decode(payload); + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -40,8 +40,8 @@ describe('CommentsUpdate', () => { }); test('it b', () => { - const payload = [getCommentsNewMock()]; - const decoded = commentsUpdateArray.decode(payload); + const payload = [getCreateCommentsMock()]; + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -50,7 +50,7 @@ describe('CommentsUpdate', () => { test('it should not validate when undefined', () => { const payload = undefined; - const decoded = commentsUpdateArray.decode(payload); + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); @@ -58,8 +58,8 @@ describe('CommentsUpdate', () => { }); test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as CommentsUpdateArray; - const decoded = commentsUpdateArray.decode(payload); + const payload = ([1] as unknown) as UpdateCommentsArray; + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -71,10 +71,10 @@ describe('CommentsUpdate', () => { }); }); - describe('commentsUpdateArrayOrUndefined', () => { + describe('updateCommentsArrayOrUndefined', () => { test('it should validate an array of comments', () => { - const payload = getCommentsUpdateArrayMock(); - const decoded = commentsUpdateArrayOrUndefined.decode(payload); + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -83,7 +83,7 @@ describe('CommentsUpdate', () => { test('it should validate when undefined', () => { const payload = undefined; - const decoded = commentsUpdateArrayOrUndefined.decode(payload); + const decoded = updateCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -91,8 +91,8 @@ describe('CommentsUpdate', () => { }); test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as CommentsUpdateArrayOrUndefined; - const decoded = commentsUpdateArray.decode(payload); + const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; + const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).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 0000000000000..4a21bfa363d45 --- /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/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 48f21f5a1c7f6..a84283aeabbba 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 { - CommentsNewArray, + CreateCommentsArray, Description, EntriesArray, ExceptionListItemSchema, @@ -25,13 +25,13 @@ import { import { getSavedObjectType, - transformNewComments, + transformCreateCommentsToComments, transformSavedObjectToExceptionListItem, } from './utils'; interface CreateExceptionListItemOptions { _tags: _Tags; - comments: CommentsNewArray; + 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: transformNewComments({ newComments: comments, user }), + comments: transformedComments, created_at: dateNow, created_by: user, description, 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 a6896117744a6..5578063fd9b6c 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 { - CommentsUpdateArrayOrUndefined, DescriptionOrUndefined, EntriesArrayOrUndefined, ExceptionListItemSchema, @@ -19,19 +18,20 @@ import { NameOrUndefined, NamespaceType, TagsOrUndefined, + UpdateCommentsArrayOrUndefined, _TagsOrUndefined, } from '../../../common/schemas'; import { getSavedObjectType, - transformCommentsUpdate, transformSavedObjectUpdateToExceptionListItem, + transformUpdateCommentsToComments, } from './utils'; import { getExceptionListItem } from './get_exception_list_item'; interface UpdateExceptionListItemOptions { id: IdOrUndefined; - comments: CommentsUpdateArrayOrUndefined; + comments: UpdateCommentsArrayOrUndefined; _tags: _TagsOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; @@ -71,16 +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: transformCommentsUpdate({ - existingComments: exceptionListItem.comments, - newComments: 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 index d458a5934fd05..bc3249826a9bb 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -8,7 +8,12 @@ import moment from 'moment'; import { DATE_NOW, USER } from '../../../common/constants.mock'; -import { isCommentEqual, transformCommentsUpdate, transformNewComments } from './utils'; +import { + isCommentEqual, + transformCreateCommentsToComments, + transformUpdateComments, + transformUpdateCommentsToComments, +} from './utils'; describe('utils', () => { const anchor = '2020-06-17T20:34:51.337Z'; @@ -23,15 +28,25 @@ describe('utils', () => { clock.restore(); }); - describe('#transformCommentsUpdate', () => { - test('it formats newly appended comments', () => { - const comments = transformCommentsUpdate({ - existingComments: [ + 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' }, ], - newComments: [ + existingComments: [ { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, - { comment: 'Im a new comment' }, ], user: 'lily', }); @@ -39,7 +54,7 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: '2020-04-20T15:25:31.830Z', + created_at: '2020-06-17T20:34:51.337Z', created_by: 'lily', }, { @@ -50,23 +65,23 @@ describe('utils', () => { ]); }); - test('it formats multiple newly appended comments', () => { - const comments = transformCommentsUpdate({ - existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, - ], - newComments: [ + 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: '2020-04-20T15:25:31.830Z', + created_at: '2020-06-17T20:34:51.337Z', created_by: 'lily', }, { @@ -83,106 +98,147 @@ describe('utils', () => { }); test('it should not throw if comments match existing comments', () => { - const comments = transformCommentsUpdate({ + 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' }, ], - newComments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }], user: 'lily', }); expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: '2020-04-20T15:25:31.830Z', + created_at: '2020-06-17T20:34:51.337Z', created_by: 'lily', }, ]); }); - test('it throws if user tries to update existing comment timestamp', () => { + 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: '2020-06-17T20:34:51.337Z', + updated_by: 'lily', + }, + ]); + }); + + test('it throws an error if user tries to delete comments', () => { expect(() => - transformCommentsUpdate({ + transformUpdateCommentsToComments({ + comments: [], existingComments: [ { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, ], - newComments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], - user: 'bane', + user: 'lily', }) ).toThrowErrorMatchingInlineSnapshot( - `"Existing comments cannot be edited, only new comments may be appended"` + `"Comments cannot be deleted, only new comments may be added"` ); }); - test('it throws if user tries to update existing comment author', () => { + test('it throws if user tries to update existing comment timestamp', () => { expect(() => - transformCommentsUpdate({ + 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!' }, + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, ], - newComments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], user: 'bane', }) - ).toThrowErrorMatchingInlineSnapshot( - `"Existing comments cannot be edited, only new comments may be appended"` - ); + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); }); - test('it throws if user tries to update existing comment', () => { + test('it throws if user tries to update existing comment author', () => { expect(() => - transformCommentsUpdate({ + 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' }, + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' }, ], - newComments: [ + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's 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', }, ], - user: 'lily', + existingComments: [ + { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + ], + user: 'bane', }) - ).toThrowErrorMatchingInlineSnapshot( - `"Existing comments cannot be edited, only new comments may be appended"` - ); + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); }); test('it throws if user tries to update order of comments', () => { expect(() => - transformCommentsUpdate({ - existingComments: [ + transformUpdateCommentsToComments({ + comments: [ + { comment: 'Im a new comment' }, { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, ], - newComments: [ - { comment: 'Im a new comment' }, + existingComments: [ { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, ], user: 'lily', }) - ).toThrowErrorMatchingInlineSnapshot( - `"Existing comments cannot be edited, only new comments may be appended"` - ); + ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`); }); test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => { expect(() => - transformCommentsUpdate({ - existingComments: [], - newComments: [ + 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 appended"`); + ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`); }); }); - describe('#transformNewComments', () => { - test('it formats newly appended comments', () => { - const comments = transformNewComments({ - newComments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }], + 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', }); @@ -201,6 +257,40 @@ describe('utils', () => { }); }); + 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', + }, + 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: '2020-06-17T20:34:51.337Z', + 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', + }, + user: 'bane', + }) + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); + }); + }); + describe('#isCommentEqual', () => { test('it returns false if "comment" values differ', () => { const result = isCommentEqual( 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 fab278bbae744..e89e3128ef52b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -9,17 +9,17 @@ import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } fro import { ErrorWithStatusCode } from '../../error_with_status_code'; import { Comments, + CommentsArray, CommentsArrayOrUndefined, - CommentsNew, - CommentsNewArrayOrUndefined, - CommentsUpdateArrayOrUndefined, + CreateComments, + CreateCommentsArrayOrUndefined, ExceptionListItemSchema, ExceptionListSchema, ExceptionListSoSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, NamespaceType, - commentsNew, + UpdateCommentsArrayOrUndefined, comments as commentsSchema, } from '../../../common/schemas'; import { @@ -261,70 +261,79 @@ export const transformSavedObjectsToFoundExceptionList = ({ * 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 | CommentsNew, - commentB: Comments | CommentsNew -): boolean => { +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 transformCommentsUpdate = ({ - newComments, +export const transformUpdateCommentsToComments = ({ + comments, existingComments, user, }: { - newComments: CommentsUpdateArrayOrUndefined; - existingComments: CommentsArrayOrUndefined; + comments: UpdateCommentsArrayOrUndefined; + existingComments: CommentsArray; user: string; -}): CommentsArrayOrUndefined => { - if ( - newComments != null && - newComments.length === 0 && - existingComments != null && - existingComments.length > 0 - ) { +}): CommentsArray => { + const newComments = comments ?? []; + + if (newComments.length < existingComments.length) { throw new ErrorWithStatusCode( - 'Comments cannot be deleted, only new comments may be appended', + 'Comments cannot be deleted, only new comments may be added', 403 ); - } else if (newComments != null) { - const existing = existingComments ?? []; + } else { return newComments.flatMap((c, index) => { - const existingComment = existing[index]; + const existingComment = existingComments[index]; - // no comments exist, and user tries to add comment of type `comment` - if (commentsSchema.is(c) && existing.length === 0) { - throw new ErrorWithStatusCode('Only new comments may be appended', 403); - } else if (index <= existing.length - 1 && !isCommentEqual(c, existingComment)) { - throw new ErrorWithStatusCode( - 'Existing comments cannot be edited, only new comments may be appended', - 403 - ); - } else if (index > existing.length - 1 && commentsNew.is(c)) { - const newComment = transformNewComments({ newComments: [c], user }); - return newComment ?? []; + 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, user }); } else { - return commentsSchema.is(c) ? c : []; + return transformCreateCommentsToComments({ comments: [c], user }) ?? []; } }); + } +}; + +export const transformUpdateComments = ({ + comment, + user, +}: { + comment: Comments; + user: string; +}): Comments => { + if (comment.created_by === user) { + const dateNow = new Date().toISOString(); + + return { + ...comment, + updated_at: dateNow, + updated_by: user, + }; } else { - return existingComments; + // existing comment is being edited, can only be edited by author + throw new ErrorWithStatusCode("Not authorized to edit other's comments", 403); } }; -export const transformNewComments = ({ - newComments, +export const transformCreateCommentsToComments = ({ + comments, user, }: { - newComments: CommentsNewArrayOrUndefined; + comments: CreateCommentsArrayOrUndefined; user: string; }): CommentsArrayOrUndefined => { const dateNow = new Date().toISOString(); - if (newComments != null) { - return newComments.map((c: CommentsNew) => { + if (comments != null) { + return comments.map((c: CreateComments) => { return { comment: c.comment, created_at: dateNow, @@ -332,6 +341,6 @@ export const transformNewComments = ({ }; }); } else { - return newComments; + return comments; } }; From d7936932916ef5cb196cf5b08fcbe87b4e89d436 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 18 Jun 2020 12:00:21 -0400 Subject: [PATCH 3/9] updated to add support for updating comments --- .../server/saved_objects/exception_list.ts | 6 +++ .../updates/simple_update_item.json | 9 +--- .../services/exception_lists/utils.test.ts | 42 ++++++++++++++----- .../server/services/exception_lists/utils.ts | 7 +++- 4 files changed, 44 insertions(+), 20 deletions(-) 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 8fb618c01213c..c6ae0d5e27d7c 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 27f020c43d1bf..c505dc64d555d 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/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts index bc3249826a9bb..963a41587d4e6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -54,12 +54,12 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, { comment: 'Im a new comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, ]); @@ -81,17 +81,17 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, { comment: 'Im a new comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, { comment: 'Im another new comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, ]); @@ -109,7 +109,7 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, ]); @@ -135,12 +135,30 @@ describe('utils', () => { comment: 'Im an old comment that is trying to be updated', created_at: DATE_NOW, created_by: 'lily', - updated_at: '2020-06-17T20:34:51.337Z', + 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({ @@ -209,7 +227,9 @@ describe('utils', () => { ], user: 'lily', }) - ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`); + ).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', () => { @@ -245,12 +265,12 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im a new comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, { comment: 'Im another new comment', - created_at: '2020-06-17T20:34:51.337Z', + created_at: anchor, created_by: 'lily', }, ]); @@ -272,7 +292,7 @@ describe('utils', () => { comment: 'Im an old comment that is trying to be updated', created_at: '2020-04-20T15:25:31.830Z', created_by: 'lily', - updated_at: '2020-06-17T20:34:51.337Z', + updated_at: anchor, updated_by: 'lily', }); }); 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 e89e3128ef52b..7ed6b3e227efe 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -288,7 +288,12 @@ export const transformUpdateCommentsToComments = ({ return newComments.flatMap((c, index) => { const existingComment = existingComments[index]; - if (commentsSchema.is(c) && existingComment == null) { + 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) && From 6e1796544aa92a7e42ed73801d4a1db4144ff785 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 18 Jun 2020 13:55:09 -0400 Subject: [PATCH 4/9] updated tests per new error formatter (woohoo) and updated logic to avoid empty comments --- .../common/schemas/types/comments.test.ts | 18 +++--- .../schemas/types/create_comments.test.ts | 18 ++++-- .../types/default_comments_array.test.ts | 8 +-- .../default_create_comments_array.test.ts | 8 ++- .../default_update_comments_array.test.ts | 12 ++-- .../schemas/types/update_comments.test.ts | 20 ++++--- .../services/exception_lists/utils.test.ts | 60 +++++++++++++++++-- .../server/services/exception_lists/utils.ts | 30 ++++++---- 8 files changed, 126 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/types/comments.test.ts b/x-pack/plugins/lists/common/schemas/types/comments.test.ts index e737fd62e0d62..29bfde03abcc8 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comments.test.ts @@ -48,8 +48,8 @@ describe('Comments', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to ""', - 'Invalid value "undefined" supplied to ""', + '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({}); }); @@ -63,7 +63,7 @@ describe('Comments', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some value" supplied to "comment"', + 'Invalid value "["some value"]" supplied to "comment"', ]); expect(message.schema).toEqual({}); }); @@ -164,7 +164,9 @@ describe('Comments', () => { const decoded = commentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + 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({}); }); @@ -174,8 +176,8 @@ describe('Comments', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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({}); }); @@ -206,8 +208,8 @@ describe('Comments', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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/create_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts index 794591a109388..d2680750e05e4 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts @@ -35,7 +35,9 @@ describe('CreateComments', () => { const decoded = createComments.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "{| comment: string |}"', + ]); expect(message.schema).toEqual({}); }); @@ -48,7 +50,7 @@ describe('CreateComments', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some value" supplied to "comment"', + 'Invalid value "["some value"]" supplied to "comment"', ]); expect(message.schema).toEqual({}); }); @@ -81,7 +83,9 @@ describe('CreateComments', () => { const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<{| comment: string |}>"', + ]); expect(message.schema).toEqual({}); }); @@ -90,7 +94,9 @@ describe('CreateComments', () => { const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + ]); expect(message.schema).toEqual({}); }); }); @@ -119,7 +125,9 @@ describe('CreateComments', () => { const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to ""']); + 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/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index d63f343c35e9c..3a4241aaec82d 100644 --- 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 @@ -39,8 +39,8 @@ describe('default_comments_array', () => { // TODO: Known weird error formatting that is on our list to address expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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({}); }); @@ -51,8 +51,8 @@ describe('default_comments_array', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to ""', - 'Invalid value "some string" supplied to ""', + '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({}); }); 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 index 1c7b5df282288..f5ef7d0ad96bd 100644 --- 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 @@ -38,7 +38,9 @@ describe('default_create_comments_array', () => { 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 ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + ]); expect(message.schema).toEqual({}); }); @@ -47,7 +49,9 @@ describe('default_create_comments_array', () => { const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<{| comment: string |}>"', + ]); expect(message.schema).toEqual({}); }); 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 index e8262a4478b42..b023e73cb9328 100644 --- 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 @@ -39,9 +39,9 @@ describe('default_update_comments_array', () => { // TODO: Known weird error formatting that is on our list to address expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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({}); }); @@ -52,9 +52,9 @@ describe('default_update_comments_array', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to ""', - 'Invalid value "some string" supplied to ""', - 'Invalid value "some string" supplied to ""', + '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({}); }); 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 index a5cc5bdbfa8fb..7668504b031b5 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts @@ -30,7 +30,7 @@ describe('CommentsUpdate', () => { expect(message.schema).toEqual(payload); }); - test('it a', () => { + test('it should validate an array of existing comments', () => { const payload = [getCommentsMock()]; const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -39,7 +39,7 @@ describe('CommentsUpdate', () => { expect(message.schema).toEqual(payload); }); - test('it b', () => { + test('it should validate an array of new comments', () => { const payload = [getCreateCommentsMock()]; const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -53,7 +53,9 @@ describe('CommentsUpdate', () => { const decoded = updateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to ""']); + 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({}); }); @@ -63,9 +65,9 @@ describe('CommentsUpdate', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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({}); }); @@ -96,9 +98,9 @@ describe('CommentsUpdate', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', - 'Invalid value "1" supplied to ""', + '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/server/services/exception_lists/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts index 963a41587d4e6..9cc2aacd88458 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -182,7 +182,7 @@ describe('utils', () => { ], user: 'bane', }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); }); test('it throws if user tries to update existing comment author', () => { @@ -194,7 +194,7 @@ describe('utils', () => { ], user: 'bane', }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); }); test('it throws if user tries to update an existing comment that is not their own', () => { @@ -212,7 +212,7 @@ describe('utils', () => { ], user: 'bane', }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit other's comments"`); + ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); }); test('it throws if user tries to update order of comments', () => { @@ -244,6 +244,21 @@ describe('utils', () => { }) ).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', () => { @@ -275,6 +290,15 @@ describe('utils', () => { }, ]); }); + + 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', () => { @@ -285,6 +309,11 @@ describe('utils', () => { created_at: DATE_NOW, created_by: 'lily', }, + existingComment: { + comment: 'Im an old comment', + created_at: DATE_NOW, + created_by: 'lily', + }, user: 'lily', }); @@ -305,9 +334,32 @@ describe('utils', () => { 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 other's comments"`); + ).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"`); }); }); 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 7ed6b3e227efe..5df4d36597c05 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -300,7 +300,7 @@ export const transformUpdateCommentsToComments = ({ existingComment != null && !isCommentEqual(c, existingComment) ) { - return transformUpdateComments({ comment: c, user }); + return transformUpdateComments({ comment: c, existingComment, user }); } else { return transformCreateCommentsToComments({ comments: [c], user }) ?? []; } @@ -310,12 +310,21 @@ export const transformUpdateCommentsToComments = ({ export const transformUpdateComments = ({ comment, + existingComment, user, }: { comment: Comments; + existingComment: Comments; user: string; }): Comments => { - if (comment.created_by === user) { + 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', 403); + } 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 { @@ -323,9 +332,6 @@ export const transformUpdateComments = ({ updated_at: dateNow, updated_by: user, }; - } else { - // existing comment is being edited, can only be edited by author - throw new ErrorWithStatusCode("Not authorized to edit other's comments", 403); } }; @@ -339,11 +345,15 @@ export const transformCreateCommentsToComments = ({ const dateNow = new Date().toISOString(); if (comments != null) { return comments.map((c: CreateComments) => { - return { - comment: c.comment, - created_at: dateNow, - created_by: user, - }; + 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; From 5b165e9ea63753e20f0f52fab751ffbfc7c1c161 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 18 Jun 2020 14:01:59 -0400 Subject: [PATCH 5/9] updated UI to use new comments structure --- .../public/common/components/exceptions/helpers.tsx | 5 +++-- .../public/common/components/exceptions/types.ts | 6 ------ .../plugins/security_solution/public/lists_plugin_deps.ts | 1 + 3 files changed, 4 insertions(+), 8 deletions(-) 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 f8b9c39801ae5..eac7dae4b6846 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, @@ -172,7 +173,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 24c328462ce2f..ed2be64b4430f 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/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index 22732c86bd9a9..a92a2dd1f193d 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -15,6 +15,7 @@ export { UseExceptionListSuccess, } from '../../lists/public'; export { + CommentsArray, ExceptionListSchema, ExceptionListItemSchema, Entry, From 5795400be49ee17df03d3241267c44cd331a8c1b Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 18 Jun 2020 14:19:38 -0400 Subject: [PATCH 6/9] clean up --- .../create_exception_list_item_schema.test.ts | 43 +++++++++++++------ .../schemas/types/create_comments.mock.ts | 5 +-- .../server/services/exception_lists/utils.ts | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) 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 bcd1218cb5663..34551b74d8c9f 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,8 @@ 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 { @@ -27,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); @@ -39,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); @@ -51,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); @@ -63,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); @@ -75,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; @@ -88,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; @@ -101,12 +103,25 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should NOT accept "comments" with "created_at" or "created_by" values', () => { + 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: [{ comment: 'some comment', created_at: 'some time', created_by: 'someone' }], + comments: [getCommentsMock()], }; const decoded = createExceptionListItemSchema.decode(inputPayload); const checked = exactCheck(inputPayload, decoded); @@ -115,7 +130,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should accept an undefined for "entries" but return an array', () => { + test('it should validate an undefined for "entries" but return an array', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.entries; @@ -128,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; @@ -141,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; @@ -154,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; @@ -167,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); @@ -179,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/types/create_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts index 7f6731071b9b8..60a59432275ca 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts @@ -9,7 +9,4 @@ export const getCreateCommentsMock = (): CreateComments => ({ comment: 'some comments', }); -export const getCreateCommentsArrayMock = (): CreateCommentsArray => [ - getCreateCommentsMock(), - getCreateCommentsMock(), -]; +export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()]; 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 5df4d36597c05..14b5309f67dc9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -319,7 +319,7 @@ export const transformUpdateComments = ({ }): 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', 403); + 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) { From a90e50fcf2398e60df117473b4a780398594d536 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 13:36:02 -0400 Subject: [PATCH 7/9] updated tests --- .../common/components/exceptions/helpers.test.tsx | 10 +++++----- .../exception_item/exception_details.test.tsx | 14 +++++++------- .../viewer/exception_item/index.test.tsx | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) 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 2239de3764326..d6b2e9f15822f 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/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 3ea8507d82a15..f5b34b7838d25 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 })}> { 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 })}> Date: Tue, 23 Jun 2020 14:12:21 -0400 Subject: [PATCH 8/9] fixed bug pointed out by pedro in api.ts --- x-pack/plugins/lists/public/exceptions/api.test.ts | 2 +- x-pack/plugins/lists/public/exceptions/api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 72a689650ea2d..975641b9bebe2 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 2ab7695d8c17c..a581cfd08ecc1 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, From f55a236cdb008229637b312385ebea6de64dd70e Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 23 Jun 2020 16:58:31 -0400 Subject: [PATCH 9/9] updated type issue in tests --- .../exception_lists/exception_list_client_types.ts | 7 ++++--- .../exceptions/viewer/exception_item/index.stories.tsx | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) 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 03f5de516561b..203d32911a6df 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/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx index b5f18feb48502..56b029aaee81e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionItem } from './'; 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'; addDecorator((storyFn) => ( ({ 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 (