mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
### Summary This PR is a follow up to #68864 . That PR used a partial to differentiate between new and existing comments, this meant that comments could be updated when they shouldn't. It was decided in our discussion of exception list schemas that comments should be append only. This PR assures that's the case, but also leaves it open to editing comments (via API). It checks to make sure that users can only update their own comments.
This commit is contained in:
parent
14699f7f0f
commit
936af29841
35 changed files with 1440 additions and 140 deletions
|
@ -8,6 +8,9 @@ import { left } from 'fp-ts/lib/Either';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
import { getCreateCommentsArrayMock } from '../types/create_comments.mock';
|
||||
import { getCommentsMock } from '../types/comments.mock';
|
||||
import { CommentsArray } from '../types';
|
||||
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
|
@ -26,7 +29,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not accept an undefined for "description"', () => {
|
||||
test('it should not validate an undefined for "description"', () => {
|
||||
const payload = getCreateExceptionListItemSchemaMock();
|
||||
delete payload.description;
|
||||
const decoded = createExceptionListItemSchema.decode(payload);
|
||||
|
@ -38,7 +41,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not accept an undefined for "name"', () => {
|
||||
test('it should not validate an undefined for "name"', () => {
|
||||
const payload = getCreateExceptionListItemSchemaMock();
|
||||
delete payload.name;
|
||||
const decoded = createExceptionListItemSchema.decode(payload);
|
||||
|
@ -50,7 +53,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not accept an undefined for "type"', () => {
|
||||
test('it should not validate an undefined for "type"', () => {
|
||||
const payload = getCreateExceptionListItemSchemaMock();
|
||||
delete payload.type;
|
||||
const decoded = createExceptionListItemSchema.decode(payload);
|
||||
|
@ -62,7 +65,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not accept an undefined for "list_id"', () => {
|
||||
test('it should not validate an undefined for "list_id"', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.list_id;
|
||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||
|
@ -74,7 +77,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
|
||||
test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
|
||||
const payload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete payload.meta;
|
||||
|
@ -87,7 +90,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.comments;
|
||||
|
@ -100,7 +103,34 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "entries" but return an array', () => {
|
||||
test('it should validate "comments" array', () => {
|
||||
const inputPayload = {
|
||||
...getCreateExceptionListItemSchemaMock(),
|
||||
comments: getCreateCommentsArrayMock(),
|
||||
};
|
||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||
const checked = exactCheck(inputPayload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
delete (message.schema as CreateExceptionListItemSchema).item_id;
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(inputPayload);
|
||||
});
|
||||
|
||||
test('it should NOT validate "comments" with "created_at" or "created_by" values', () => {
|
||||
const inputPayload: Omit<CreateExceptionListItemSchema, 'comments'> & {
|
||||
comments?: CommentsArray;
|
||||
} = {
|
||||
...getCreateExceptionListItemSchemaMock(),
|
||||
comments: [getCommentsMock()],
|
||||
};
|
||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||
const checked = exactCheck(inputPayload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should validate an undefined for "entries" but return an array', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.entries;
|
||||
|
@ -113,7 +143,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
|
||||
test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.namespace_type;
|
||||
|
@ -126,7 +156,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.tags;
|
||||
|
@ -139,7 +169,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload._tags;
|
||||
|
@ -152,7 +182,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(outputPayload);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "item_id" and auto generate a uuid', () => {
|
||||
test('it should validate an undefined for "item_id" and auto generate a uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.item_id;
|
||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||
|
@ -164,7 +194,7 @@ describe('create_exception_list_item_schema', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => {
|
||||
test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => {
|
||||
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||
delete inputPayload.item_id;
|
||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
tags,
|
||||
} from '../common/schemas';
|
||||
import { Identity, RequiredKeepUndefined } from '../../types';
|
||||
import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types';
|
||||
import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types';
|
||||
import { EntriesArray } from '../types/entries';
|
||||
import { DefaultUuid } from '../../siem_common_deps';
|
||||
|
||||
|
@ -39,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([
|
|||
t.exact(
|
||||
t.partial({
|
||||
_tags, // defaults to empty array if not set during decode
|
||||
comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode
|
||||
comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode
|
||||
entries: DefaultEntryArray, // defaults to empty array if not set during decode
|
||||
item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode
|
||||
meta, // defaults to undefined if not set during decode
|
||||
|
@ -63,7 +63,7 @@ export type CreateExceptionListItemSchemaDecoded = Identity<
|
|||
'_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
|
||||
> & {
|
||||
_tags: _Tags;
|
||||
comments: CommentsPartialArray;
|
||||
comments: CreateCommentsArray;
|
||||
tags: Tags;
|
||||
item_id: ItemId;
|
||||
entries: EntriesArray;
|
||||
|
|
|
@ -23,10 +23,10 @@ import {
|
|||
} from '../common/schemas';
|
||||
import { Identity, RequiredKeepUndefined } from '../../types';
|
||||
import {
|
||||
CommentsPartialArray,
|
||||
DefaultCommentsPartialArray,
|
||||
DefaultEntryArray,
|
||||
DefaultUpdateCommentsArray,
|
||||
EntriesArray,
|
||||
UpdateCommentsArray,
|
||||
} from '../types';
|
||||
|
||||
export const updateExceptionListItemSchema = t.intersection([
|
||||
|
@ -40,7 +40,7 @@ export const updateExceptionListItemSchema = t.intersection([
|
|||
t.exact(
|
||||
t.partial({
|
||||
_tags, // defaults to empty array if not set during decode
|
||||
comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode
|
||||
comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode
|
||||
entries: DefaultEntryArray, // defaults to empty array if not set during decode
|
||||
id, // defaults to undefined if not set during decode
|
||||
item_id: t.union([t.string, t.undefined]),
|
||||
|
@ -65,7 +65,7 @@ export type UpdateExceptionListItemSchemaDecoded = Identity<
|
|||
'_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments'
|
||||
> & {
|
||||
_tags: _Tags;
|
||||
comments: CommentsPartialArray;
|
||||
comments: UpdateCommentsArray;
|
||||
tags: Tags;
|
||||
entries: EntriesArray;
|
||||
namespace_type: NamespaceType;
|
||||
|
|
|
@ -6,17 +6,12 @@
|
|||
|
||||
import { DATE_NOW, USER } from '../../constants.mock';
|
||||
|
||||
import { CommentsArray } from './comments';
|
||||
import { Comments, CommentsArray } from './comments';
|
||||
|
||||
export const getCommentsMock = (): CommentsArray => [
|
||||
{
|
||||
comment: 'some comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
},
|
||||
{
|
||||
comment: 'some other comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
];
|
||||
export const getCommentsMock = (): Comments => ({
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
});
|
||||
|
||||
export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()];
|
||||
|
|
217
x-pack/plugins/lists/common/schemas/types/comments.test.ts
Normal file
217
x-pack/plugins/lists/common/schemas/types/comments.test.ts
Normal file
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { DATE_NOW } from '../../constants.mock';
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { getCommentsArrayMock, getCommentsMock } from './comments.mock';
|
||||
import {
|
||||
Comments,
|
||||
CommentsArray,
|
||||
CommentsArrayOrUndefined,
|
||||
comments,
|
||||
commentsArray,
|
||||
commentsArrayOrUndefined,
|
||||
} from './comments';
|
||||
|
||||
describe('Comments', () => {
|
||||
describe('comments', () => {
|
||||
test('it should validate a comments', () => {
|
||||
const payload = getCommentsMock();
|
||||
const decoded = comments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate with "updated_at" and "updated_by"', () => {
|
||||
const payload = getCommentsMock();
|
||||
payload.updated_at = DATE_NOW;
|
||||
payload.updated_by = 'someone';
|
||||
const decoded = comments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = comments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"',
|
||||
'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate when "comment" is not a string', () => {
|
||||
const payload: Omit<Comments, 'comment'> & { 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<Comments, 'created_at'> & { 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<Comments, 'created_by'> & { 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<Comments, 'updated_at'> & { 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<Comments, 'updated_by'> & { updated_by: number } = {
|
||||
...getCommentsMock(),
|
||||
updated_by: 1,
|
||||
};
|
||||
const decoded = comments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "updated_by"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should strip out extra keys', () => {
|
||||
const payload: Comments & {
|
||||
extraKey?: string;
|
||||
} = getCommentsMock();
|
||||
payload.extraKey = 'some value';
|
||||
const decoded = comments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(getCommentsMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('commentsArray', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const decoded = commentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate when a comments includes "updated_at" and "updated_by"', () => {
|
||||
const commentsPayload = getCommentsMock();
|
||||
commentsPayload.updated_at = DATE_NOW;
|
||||
commentsPayload.updated_by = 'someone';
|
||||
const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()];
|
||||
const decoded = commentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = commentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as CommentsArray;
|
||||
const decoded = commentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('commentsArrayOrUndefined', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const decoded = commentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = commentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as CommentsArrayOrUndefined;
|
||||
const decoded = commentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<typeof commentsArray>;
|
||||
export type Comment = t.TypeOf<typeof comment>;
|
||||
export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
|
||||
export type CommentsArrayOrUndefined = t.TypeOf<typeof commentsArrayOrUndefined>;
|
||||
|
||||
export const commentPartial = t.intersection([
|
||||
export const comments = t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
comment: t.string,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
created_at: t.string, // TODO: Make this into an ISO Date string check,
|
||||
created_by: t.string,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
updated_at: t.string,
|
||||
updated_by: t.string,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
export const commentsPartialArray = t.array(commentPartial);
|
||||
export type CommentsPartialArray = t.TypeOf<typeof commentsPartialArray>;
|
||||
export type CommentPartial = t.TypeOf<typeof commentPartial>;
|
||||
export const commentsPartialArrayOrUndefined = t.union([commentsPartialArray, t.undefined]);
|
||||
export type CommentsPartialArrayOrUndefined = t.TypeOf<typeof commentsPartialArrayOrUndefined>;
|
||||
export const commentsArray = t.array(comments);
|
||||
export type CommentsArray = t.TypeOf<typeof commentsArray>;
|
||||
export type Comments = t.TypeOf<typeof comments>;
|
||||
export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
|
||||
export type CommentsArrayOrUndefined = t.TypeOf<typeof commentsArrayOrUndefined>;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { CreateComments, CreateCommentsArray } from './create_comments';
|
||||
|
||||
export const getCreateCommentsMock = (): CreateComments => ({
|
||||
comment: 'some comments',
|
||||
});
|
||||
|
||||
export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()];
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comments.mock';
|
||||
import {
|
||||
CreateComments,
|
||||
CreateCommentsArray,
|
||||
CreateCommentsArrayOrUndefined,
|
||||
createComments,
|
||||
createCommentsArray,
|
||||
createCommentsArrayOrUndefined,
|
||||
} from './create_comments';
|
||||
|
||||
describe('CreateComments', () => {
|
||||
describe('createComments', () => {
|
||||
test('it should validate a comments', () => {
|
||||
const payload = getCreateCommentsMock();
|
||||
const decoded = createComments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = createComments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "{| comment: string |}"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate when "comment" is not a string', () => {
|
||||
const payload: Omit<CreateComments, 'comment'> & { comment: string[] } = {
|
||||
...getCreateCommentsMock(),
|
||||
comment: ['some value'],
|
||||
};
|
||||
const decoded = createComments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "["some value"]" supplied to "comment"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should strip out extra keys', () => {
|
||||
const payload: CreateComments & {
|
||||
extraKey?: string;
|
||||
} = getCreateCommentsMock();
|
||||
payload.extraKey = 'some value';
|
||||
const decoded = createComments.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(getCreateCommentsMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCommentsArray', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getCreateCommentsArrayMock();
|
||||
const decoded = createCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = createCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "Array<{| comment: string |}>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as CreateCommentsArray;
|
||||
const decoded = createCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<{| comment: string |}>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCommentsArrayOrUndefined', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getCreateCommentsArrayMock();
|
||||
const decoded = createCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = createCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined;
|
||||
const decoded = createCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<{| comment: string |}>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
18
x-pack/plugins/lists/common/schemas/types/create_comments.ts
Normal file
18
x-pack/plugins/lists/common/schemas/types/create_comments.ts
Normal file
|
@ -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<typeof createCommentsArray>;
|
||||
export type CreateComments = t.TypeOf<typeof createComments>;
|
||||
export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]);
|
||||
export type CreateCommentsArrayOrUndefined = t.TypeOf<typeof createCommentsArrayOrUndefined>;
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { DefaultCommentsArray } from './default_comments_array';
|
||||
import { CommentsArray } from './comments';
|
||||
import { getCommentsArrayMock } from './comments.mock';
|
||||
|
||||
describe('default_comments_array', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
const payload: CommentsArray = [];
|
||||
const decoded = DefaultCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload: CommentsArray = getCommentsArrayMock();
|
||||
const decoded = DefaultCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of numbers', () => {
|
||||
const payload = [1];
|
||||
const decoded = DefaultCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
// TODO: Known weird error formatting that is on our list to address
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of strings', () => {
|
||||
const payload = ['some string'];
|
||||
const decoded = DefaultCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default array entry', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -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<CommentsArray, CommentsArray, unknown>;
|
||||
export type DefaultCommentsPartialArrayC = t.Type<
|
||||
CommentsPartialArray,
|
||||
CommentsPartialArray,
|
||||
unknown
|
||||
>;
|
||||
|
||||
/**
|
||||
* Types the DefaultCommentsArray as:
|
||||
|
@ -26,24 +21,8 @@ export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type<
|
|||
unknown
|
||||
>(
|
||||
'DefaultCommentsArray',
|
||||
t.array(comment).is,
|
||||
(input, context): Either<t.Errors, CommentsArray> =>
|
||||
input == null ? t.success([]) : t.array(comment).validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
|
||||
/**
|
||||
* Types the DefaultCommentsPartialArray as:
|
||||
* - If null or undefined, then a default array of type entry will be set
|
||||
*/
|
||||
export const DefaultCommentsPartialArray: DefaultCommentsPartialArrayC = new t.Type<
|
||||
CommentsPartialArray,
|
||||
CommentsPartialArray,
|
||||
unknown
|
||||
>(
|
||||
'DefaultCommentsPartialArray',
|
||||
t.array(commentPartial).is,
|
||||
(input, context): Either<t.Errors, CommentsPartialArray> =>
|
||||
input == null ? t.success([]) : t.array(commentPartial).validate(input, context),
|
||||
t.array(comments).is,
|
||||
(input): Either<t.Errors, CommentsArray> =>
|
||||
input == null ? t.success([]) : t.array(comments).decode(input),
|
||||
t.identity
|
||||
);
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { DefaultCreateCommentsArray } from './default_create_comments_array';
|
||||
import { CreateCommentsArray } from './create_comments';
|
||||
import { getCreateCommentsArrayMock } from './create_comments.mock';
|
||||
|
||||
describe('default_create_comments_array', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
const payload: CreateCommentsArray = [];
|
||||
const decoded = DefaultCreateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload: CreateCommentsArray = getCreateCommentsArrayMock();
|
||||
const decoded = DefaultCreateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of numbers', () => {
|
||||
const payload = [1];
|
||||
const decoded = DefaultCreateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
// TODO: Known weird error formatting that is on our list to address
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<{| comment: string |}>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of strings', () => {
|
||||
const payload = ['some string'];
|
||||
const decoded = DefaultCreateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "Array<{| comment: string |}>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default array entry', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultCreateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -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<CreateCommentsArray, CreateCommentsArray, unknown>;
|
||||
|
||||
/**
|
||||
* 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<t.Errors, CreateCommentsArray> =>
|
||||
input == null ? t.success([]) : t.array(createComments).decode(input),
|
||||
t.identity
|
||||
);
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { DefaultUpdateCommentsArray } from './default_update_comments_array';
|
||||
import { UpdateCommentsArray } from './update_comments';
|
||||
import { getUpdateCommentsArrayMock } from './update_comments.mock';
|
||||
|
||||
describe('default_update_comments_array', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
const payload: UpdateCommentsArray = [];
|
||||
const decoded = DefaultUpdateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload: UpdateCommentsArray = getUpdateCommentsArrayMock();
|
||||
const decoded = DefaultUpdateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of numbers', () => {
|
||||
const payload = [1];
|
||||
const decoded = DefaultUpdateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
// TODO: Known weird error formatting that is on our list to address
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate an array of strings', () => {
|
||||
const payload = ['some string'];
|
||||
const decoded = DefaultUpdateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default array entry', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultUpdateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
|
||||
import { UpdateCommentsArray, updateCommentsArray } from './update_comments';
|
||||
|
||||
export type DefaultUpdateCommentsArrayC = t.Type<UpdateCommentsArray, UpdateCommentsArray, unknown>;
|
||||
|
||||
/**
|
||||
* Types the DefaultCommentsUpdate as:
|
||||
* - If null or undefined, then a default array of type entry will be set
|
||||
*/
|
||||
export const DefaultUpdateCommentsArray: DefaultUpdateCommentsArrayC = new t.Type<
|
||||
UpdateCommentsArray,
|
||||
UpdateCommentsArray,
|
||||
unknown
|
||||
>(
|
||||
'DefaultCreateComments',
|
||||
updateCommentsArray.is,
|
||||
(input): Either<t.Errors, UpdateCommentsArray> =>
|
||||
input == null ? t.success([]) : updateCommentsArray.decode(input),
|
||||
t.identity
|
||||
);
|
|
@ -3,8 +3,12 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export * from './default_comments_array';
|
||||
export * from './default_entries_array';
|
||||
export * from './default_namespace';
|
||||
export * from './comments';
|
||||
export * from './create_comments';
|
||||
export * from './update_comments';
|
||||
export * from './default_comments_array';
|
||||
export * from './default_create_comments_array';
|
||||
export * from './default_update_comments_array';
|
||||
export * from './default_namespace';
|
||||
export * from './default_entries_array';
|
||||
export * from './entries';
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getCommentsMock } from './comments.mock';
|
||||
import { getCreateCommentsMock } from './create_comments.mock';
|
||||
import { UpdateCommentsArray } from './update_comments';
|
||||
|
||||
export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [
|
||||
getCommentsMock(),
|
||||
getCreateCommentsMock(),
|
||||
];
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../siem_common_deps';
|
||||
|
||||
import { getUpdateCommentsArrayMock } from './update_comments.mock';
|
||||
import {
|
||||
UpdateCommentsArray,
|
||||
UpdateCommentsArrayOrUndefined,
|
||||
updateCommentsArray,
|
||||
updateCommentsArrayOrUndefined,
|
||||
} from './update_comments';
|
||||
import { getCommentsMock } from './comments.mock';
|
||||
import { getCreateCommentsMock } from './create_comments.mock';
|
||||
|
||||
describe('CommentsUpdate', () => {
|
||||
describe('updateCommentsArray', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getUpdateCommentsArrayMock();
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an array of existing comments', () => {
|
||||
const payload = [getCommentsMock()];
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an array of new comments', () => {
|
||||
const payload = [getCreateCommentsMock()];
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as UpdateCommentsArray;
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCommentsArrayOrUndefined', () => {
|
||||
test('it should validate an array of comments', () => {
|
||||
const payload = getUpdateCommentsArrayMock();
|
||||
const decoded = updateCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = updateCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate when array includes non comments types', () => {
|
||||
const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined;
|
||||
const decoded = updateCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
14
x-pack/plugins/lists/common/schemas/types/update_comments.ts
Normal file
14
x-pack/plugins/lists/common/schemas/types/update_comments.ts
Normal file
|
@ -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<typeof updateCommentsArray>;
|
||||
export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]);
|
||||
export type UpdateCommentsArrayOrUndefined = t.TypeOf<typeof updateCommentsArrayOrUndefined>;
|
|
@ -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,
|
||||
|
|
|
@ -176,7 +176,7 @@ export const updateExceptionListItem = async ({
|
|||
|
||||
if (validatedRequest != null) {
|
||||
try {
|
||||
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
|
||||
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
|
||||
body: JSON.stringify(listItem),
|
||||
method: 'PUT',
|
||||
signal,
|
||||
|
|
|
@ -77,6 +77,12 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
|
|||
created_by: {
|
||||
type: 'keyword',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'keyword',
|
||||
},
|
||||
updated_by: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
|||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
CommentsPartialArray,
|
||||
CreateCommentsArray,
|
||||
Description,
|
||||
EntriesArray,
|
||||
ExceptionListItemSchema,
|
||||
|
@ -25,13 +25,13 @@ import {
|
|||
|
||||
import {
|
||||
getSavedObjectType,
|
||||
transformComments,
|
||||
transformCreateCommentsToComments,
|
||||
transformSavedObjectToExceptionListItem,
|
||||
} from './utils';
|
||||
|
||||
interface CreateExceptionListItemOptions {
|
||||
_tags: _Tags;
|
||||
comments: CommentsPartialArray;
|
||||
comments: CreateCommentsArray;
|
||||
listId: ListId;
|
||||
itemId: ItemId;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -64,9 +64,10 @@ export const createExceptionListItem = async ({
|
|||
}: CreateExceptionListItemOptions): Promise<ExceptionListItemSchema> => {
|
||||
const savedObjectType = getSavedObjectType({ namespaceType });
|
||||
const dateNow = new Date().toISOString();
|
||||
const transformedComments = transformCreateCommentsToComments({ comments, user });
|
||||
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
|
||||
_tags,
|
||||
comments: transformComments({ comments, user }),
|
||||
comments: transformedComments,
|
||||
created_at: dateNow,
|
||||
created_by: user,
|
||||
description,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
|
||||
import {
|
||||
CommentsPartialArray,
|
||||
DescriptionOrUndefined,
|
||||
EntriesArrayOrUndefined,
|
||||
ExceptionListItemSchema,
|
||||
|
@ -19,19 +18,20 @@ import {
|
|||
NameOrUndefined,
|
||||
NamespaceType,
|
||||
TagsOrUndefined,
|
||||
UpdateCommentsArrayOrUndefined,
|
||||
_TagsOrUndefined,
|
||||
} from '../../../common/schemas';
|
||||
|
||||
import {
|
||||
getSavedObjectType,
|
||||
transformComments,
|
||||
transformSavedObjectUpdateToExceptionListItem,
|
||||
transformUpdateCommentsToComments,
|
||||
} from './utils';
|
||||
import { getExceptionListItem } from './get_exception_list_item';
|
||||
|
||||
interface UpdateExceptionListItemOptions {
|
||||
id: IdOrUndefined;
|
||||
comments: CommentsPartialArray;
|
||||
comments: UpdateCommentsArrayOrUndefined;
|
||||
_tags: _TagsOrUndefined;
|
||||
name: NameOrUndefined;
|
||||
description: DescriptionOrUndefined;
|
||||
|
@ -71,12 +71,17 @@ export const updateExceptionListItem = async ({
|
|||
if (exceptionListItem == null) {
|
||||
return null;
|
||||
} else {
|
||||
const transformedComments = transformUpdateCommentsToComments({
|
||||
comments,
|
||||
existingComments: exceptionListItem.comments,
|
||||
user,
|
||||
});
|
||||
const savedObject = await savedObjectsClient.update<ExceptionListSoSchema>(
|
||||
savedObjectType,
|
||||
exceptionListItem.id,
|
||||
{
|
||||
_tags,
|
||||
comments: transformComments({ comments, user }),
|
||||
comments: transformedComments,
|
||||
description,
|
||||
entries,
|
||||
meta,
|
||||
|
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import sinon from 'sinon';
|
||||
import moment from 'moment';
|
||||
|
||||
import { DATE_NOW, USER } from '../../../common/constants.mock';
|
||||
|
||||
import {
|
||||
isCommentEqual,
|
||||
transformCreateCommentsToComments,
|
||||
transformUpdateComments,
|
||||
transformUpdateCommentsToComments,
|
||||
} from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
const anchor = '2020-06-17T20:34:51.337Z';
|
||||
const unix = moment(anchor).valueOf();
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(unix);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
describe('#transformUpdateCommentsToComments', () => {
|
||||
test('it returns empty array if "comments" is undefined and no comments exist', () => {
|
||||
const comments = transformUpdateCommentsToComments({
|
||||
comments: undefined,
|
||||
existingComments: [],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([]);
|
||||
});
|
||||
|
||||
test('it formats newly added comments', () => {
|
||||
const comments = transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
{ comment: 'Im a new comment' },
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([
|
||||
{
|
||||
comment: 'Im an old comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
{
|
||||
comment: 'Im a new comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it formats multiple newly added comments', () => {
|
||||
const comments = transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
{ comment: 'Im a new comment' },
|
||||
{ comment: 'Im another new comment' },
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([
|
||||
{
|
||||
comment: 'Im an old comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
{
|
||||
comment: 'Im a new comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
{
|
||||
comment: 'Im another new comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should not throw if comments match existing comments', () => {
|
||||
const comments = transformUpdateCommentsToComments({
|
||||
comments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([
|
||||
{
|
||||
comment: 'Im an old comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it does not throw if user tries to update one of their own existing comments', () => {
|
||||
const comments = transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([
|
||||
{
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
updated_at: anchor,
|
||||
updated_by: 'lily',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it throws an error if user tries to update their comment, without passing in the "created_at" and "created_by" properties', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
},
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"`
|
||||
);
|
||||
});
|
||||
|
||||
test('it throws an error if user tries to delete comments', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Comments cannot be deleted, only new comments may be added"`
|
||||
);
|
||||
});
|
||||
|
||||
test('it throws if user tries to update existing comment timestamp', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'bane',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
|
||||
});
|
||||
|
||||
test('it throws if user tries to update existing comment author', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' },
|
||||
],
|
||||
user: 'bane',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
|
||||
});
|
||||
|
||||
test('it throws if user tries to update an existing comment that is not their own', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'bane',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
|
||||
});
|
||||
|
||||
test('it throws if user tries to update order of comments', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{ comment: 'Im a new comment' },
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"`
|
||||
);
|
||||
});
|
||||
|
||||
test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
{ comment: 'Im a new comment' },
|
||||
],
|
||||
existingComments: [],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`);
|
||||
});
|
||||
|
||||
test('it throws if empty comment exists', () => {
|
||||
expect(() =>
|
||||
transformUpdateCommentsToComments({
|
||||
comments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
{ comment: ' ' },
|
||||
],
|
||||
existingComments: [
|
||||
{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
|
||||
],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#transformCreateCommentsToComments', () => {
|
||||
test('it returns "undefined" if "comments" is "undefined"', () => {
|
||||
const comments = transformCreateCommentsToComments({
|
||||
comments: undefined,
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toBeUndefined();
|
||||
});
|
||||
|
||||
test('it formats newly added comments', () => {
|
||||
const comments = transformCreateCommentsToComments({
|
||||
comments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }],
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual([
|
||||
{
|
||||
comment: 'Im a new comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
{
|
||||
comment: 'Im another new comment',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it throws an error if user tries to add an empty comment', () => {
|
||||
expect(() =>
|
||||
transformCreateCommentsToComments({
|
||||
comments: [{ comment: ' ' }],
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#transformUpdateComments', () => {
|
||||
test('it updates comment and adds "updated_at" and "updated_by"', () => {
|
||||
const comments = transformUpdateComments({
|
||||
comment: {
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
existingComment: {
|
||||
comment: 'Im an old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
user: 'lily',
|
||||
});
|
||||
|
||||
expect(comments).toEqual({
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_by: 'lily',
|
||||
updated_at: anchor,
|
||||
updated_by: 'lily',
|
||||
});
|
||||
});
|
||||
|
||||
test('it throws if user tries to update an existing comment that is not their own', () => {
|
||||
expect(() =>
|
||||
transformUpdateComments({
|
||||
comment: {
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
existingComment: {
|
||||
comment: 'Im an old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
user: 'bane',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
|
||||
});
|
||||
|
||||
test('it throws if user tries to update an existing comments timestamp', () => {
|
||||
expect(() =>
|
||||
transformUpdateComments({
|
||||
comment: {
|
||||
comment: 'Im an old comment that is trying to be updated',
|
||||
created_at: anchor,
|
||||
created_by: 'lily',
|
||||
},
|
||||
existingComment: {
|
||||
comment: 'Im an old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
},
|
||||
user: 'lily',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Unable to update comment"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isCommentEqual', () => {
|
||||
test('it returns false if "comment" values differ', () => {
|
||||
const result = isCommentEqual(
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
},
|
||||
{
|
||||
comment: 'some older comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it returns false if "created_at" values differ', () => {
|
||||
const result = isCommentEqual(
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
},
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: anchor,
|
||||
created_by: USER,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it returns false if "created_by" values differ', () => {
|
||||
const result = isCommentEqual(
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
},
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: 'lily',
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it returns true if comment values are equivalent', () => {
|
||||
const result = isCommentEqual(
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
},
|
||||
{
|
||||
created_at: DATE_NOW,
|
||||
created_by: USER,
|
||||
// Disabling to assure that order doesn't matter
|
||||
// eslint-disable-next-line sort-keys
|
||||
comment: 'some old comment',
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,15 +6,21 @@
|
|||
|
||||
import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
|
||||
|
||||
import { ErrorWithStatusCode } from '../../error_with_status_code';
|
||||
import {
|
||||
Comments,
|
||||
CommentsArray,
|
||||
CommentsArrayOrUndefined,
|
||||
CommentsPartialArrayOrUndefined,
|
||||
CreateComments,
|
||||
CreateCommentsArrayOrUndefined,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
ExceptionListSoSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
FoundExceptionListSchema,
|
||||
NamespaceType,
|
||||
UpdateCommentsArrayOrUndefined,
|
||||
comments as commentsSchema,
|
||||
} from '../../../common/schemas';
|
||||
import {
|
||||
SavedObjectType,
|
||||
|
@ -251,21 +257,103 @@ export const transformSavedObjectsToFoundExceptionList = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const transformComments = ({
|
||||
/*
|
||||
* Determines whether two comments are equal, this is a very
|
||||
* naive implementation, not meant to be used for deep equality of complex objects
|
||||
*/
|
||||
export const isCommentEqual = (commentA: Comments, commentB: Comments): boolean => {
|
||||
const a = Object.values(commentA).sort().join();
|
||||
const b = Object.values(commentB).sort().join();
|
||||
|
||||
return a === b;
|
||||
};
|
||||
|
||||
export const transformUpdateCommentsToComments = ({
|
||||
comments,
|
||||
existingComments,
|
||||
user,
|
||||
}: {
|
||||
comments: UpdateCommentsArrayOrUndefined;
|
||||
existingComments: CommentsArray;
|
||||
user: string;
|
||||
}): CommentsArray => {
|
||||
const newComments = comments ?? [];
|
||||
|
||||
if (newComments.length < existingComments.length) {
|
||||
throw new ErrorWithStatusCode(
|
||||
'Comments cannot be deleted, only new comments may be added',
|
||||
403
|
||||
);
|
||||
} else {
|
||||
return newComments.flatMap((c, index) => {
|
||||
const existingComment = existingComments[index];
|
||||
|
||||
if (commentsSchema.is(existingComment) && !commentsSchema.is(c)) {
|
||||
throw new ErrorWithStatusCode(
|
||||
'When trying to update a comment, "created_at" and "created_by" must be present',
|
||||
403
|
||||
);
|
||||
} else if (commentsSchema.is(c) && existingComment == null) {
|
||||
throw new ErrorWithStatusCode('Only new comments may be added', 403);
|
||||
} else if (
|
||||
commentsSchema.is(c) &&
|
||||
existingComment != null &&
|
||||
!isCommentEqual(c, existingComment)
|
||||
) {
|
||||
return transformUpdateComments({ comment: c, existingComment, user });
|
||||
} else {
|
||||
return transformCreateCommentsToComments({ comments: [c], user }) ?? [];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const transformUpdateComments = ({
|
||||
comment,
|
||||
existingComment,
|
||||
user,
|
||||
}: {
|
||||
comment: Comments;
|
||||
existingComment: Comments;
|
||||
user: string;
|
||||
}): Comments => {
|
||||
if (comment.created_by !== user) {
|
||||
// existing comment is being edited, can only be edited by author
|
||||
throw new ErrorWithStatusCode('Not authorized to edit others comments', 401);
|
||||
} else if (existingComment.created_at !== comment.created_at) {
|
||||
throw new ErrorWithStatusCode('Unable to update comment', 403);
|
||||
} else if (comment.comment.trim().length === 0) {
|
||||
throw new ErrorWithStatusCode('Empty comments not allowed', 403);
|
||||
} else {
|
||||
const dateNow = new Date().toISOString();
|
||||
|
||||
return {
|
||||
...comment,
|
||||
updated_at: dateNow,
|
||||
updated_by: user,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const transformCreateCommentsToComments = ({
|
||||
comments,
|
||||
user,
|
||||
}: {
|
||||
comments: CommentsPartialArrayOrUndefined;
|
||||
comments: CreateCommentsArrayOrUndefined;
|
||||
user: string;
|
||||
}): CommentsArrayOrUndefined => {
|
||||
const dateNow = new Date().toISOString();
|
||||
if (comments != null) {
|
||||
return comments.map((comment) => {
|
||||
return {
|
||||
comment: comment.comment,
|
||||
created_at: comment.created_at ?? dateNow,
|
||||
created_by: comment.created_by ?? user,
|
||||
};
|
||||
return comments.map((c: CreateComments) => {
|
||||
if (c.comment.trim().length === 0) {
|
||||
throw new ErrorWithStatusCode('Empty comments not allowed', 403);
|
||||
} else {
|
||||
return {
|
||||
comment: c.comment,
|
||||
created_at: dateNow,
|
||||
created_by: user,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return comments;
|
||||
|
|
|
@ -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<React.ReactElement>(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<React.ReactElement>(result[0].children as React.ReactElement);
|
||||
|
||||
expect(wrapper.text()).toEqual('some comment');
|
||||
expect(wrapper.text()).toEqual('some old comment');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,9 +10,10 @@ import { capitalize } from 'lodash';
|
|||
import moment from 'moment';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { FormattedEntry, OperatorOption, DescriptionListItem, Comment } from './types';
|
||||
import { FormattedEntry, OperatorOption, DescriptionListItem } from './types';
|
||||
import { EXCEPTION_OPERATORS, isOperator } from './operators';
|
||||
import {
|
||||
CommentsArray,
|
||||
Entry,
|
||||
EntriesArray,
|
||||
ExceptionListItemSchema,
|
||||
|
@ -183,7 +184,7 @@ export const getDescriptionListContent = (
|
|||
*
|
||||
* @param comments ExceptionItem.comments
|
||||
*/
|
||||
export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] =>
|
||||
export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] =>
|
||||
comments.map((comment) => ({
|
||||
username: comment.created_by,
|
||||
timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'),
|
||||
|
|
|
@ -26,12 +26,6 @@ export interface DescriptionListItem {
|
|||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export enum ExceptionListType {
|
||||
DETECTION_ENGINE = 'detection',
|
||||
ENDPOINT = 'endpoint',
|
||||
|
|
|
@ -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(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
@ -60,7 +60,7 @@ describe('ExceptionDetails', () => {
|
|||
|
||||
test('it renders correct number of comments', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = [getCommentsMock()[0]];
|
||||
exceptionItem.comments = [getCommentsArrayMock()[0]];
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
@ -78,7 +78,7 @@ describe('ExceptionDetails', () => {
|
|||
|
||||
test('it renders comments plural if more than one', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
@ -96,7 +96,7 @@ describe('ExceptionDetails', () => {
|
|||
|
||||
test('it renders comments show text if "showComments" is false', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
@ -114,7 +114,7 @@ describe('ExceptionDetails', () => {
|
|||
|
||||
test('it renders comments hide text if "showComments" is true', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
@ -133,7 +133,7 @@ describe('ExceptionDetails', () => {
|
|||
test('it invokes "onCommentsClick" when comments button clicked', () => {
|
||||
const mockOnCommentsClick = jest.fn();
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionDetails
|
||||
|
|
|
@ -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) => (
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
||||
|
@ -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 (
|
||||
<ExceptionItem
|
||||
loadingItemIds={[]}
|
||||
|
|
|
@ -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';
|
||||
|
||||
describe('ExceptionItem', () => {
|
||||
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(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionItem
|
||||
|
@ -102,7 +102,7 @@ describe('ExceptionItem', () => {
|
|||
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(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ExceptionItem
|
||||
|
|
|
@ -15,6 +15,7 @@ export {
|
|||
UseExceptionListSuccess,
|
||||
} from '../../lists/public';
|
||||
export {
|
||||
CommentsArray,
|
||||
ExceptionListSchema,
|
||||
ExceptionListItemSchema,
|
||||
Entry,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue