[SIEM][Exceptions] - Cleaned up and updated exception list item comment structure (#69532)
### 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
8aa2206e04
commit
e4043b736b
|
@ -8,6 +8,9 @@ import { left } from 'fp-ts/lib/Either';
|
||||||
import { pipe } from 'fp-ts/lib/pipeable';
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
|
|
||||||
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
|
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 {
|
import {
|
||||||
CreateExceptionListItemSchema,
|
CreateExceptionListItemSchema,
|
||||||
|
@ -26,7 +29,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(payload);
|
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();
|
const payload = getCreateExceptionListItemSchemaMock();
|
||||||
delete payload.description;
|
delete payload.description;
|
||||||
const decoded = createExceptionListItemSchema.decode(payload);
|
const decoded = createExceptionListItemSchema.decode(payload);
|
||||||
|
@ -38,7 +41,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual({});
|
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();
|
const payload = getCreateExceptionListItemSchemaMock();
|
||||||
delete payload.name;
|
delete payload.name;
|
||||||
const decoded = createExceptionListItemSchema.decode(payload);
|
const decoded = createExceptionListItemSchema.decode(payload);
|
||||||
|
@ -50,7 +53,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual({});
|
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();
|
const payload = getCreateExceptionListItemSchemaMock();
|
||||||
delete payload.type;
|
delete payload.type;
|
||||||
const decoded = createExceptionListItemSchema.decode(payload);
|
const decoded = createExceptionListItemSchema.decode(payload);
|
||||||
|
@ -62,7 +65,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual({});
|
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();
|
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.list_id;
|
delete inputPayload.list_id;
|
||||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||||
|
@ -74,7 +77,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual({});
|
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 payload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete payload.meta;
|
delete payload.meta;
|
||||||
|
@ -87,7 +90,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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 inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.comments;
|
delete inputPayload.comments;
|
||||||
|
@ -100,7 +103,34 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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 inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.entries;
|
delete inputPayload.entries;
|
||||||
|
@ -113,7 +143,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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 inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.namespace_type;
|
delete inputPayload.namespace_type;
|
||||||
|
@ -126,7 +156,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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 inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.tags;
|
delete inputPayload.tags;
|
||||||
|
@ -139,7 +169,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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 inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
const outputPayload = getCreateExceptionListItemSchemaMock();
|
const outputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload._tags;
|
delete inputPayload._tags;
|
||||||
|
@ -152,7 +182,7 @@ describe('create_exception_list_item_schema', () => {
|
||||||
expect(message.schema).toEqual(outputPayload);
|
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();
|
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.item_id;
|
delete inputPayload.item_id;
|
||||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
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();
|
const inputPayload = getCreateExceptionListItemSchemaMock();
|
||||||
delete inputPayload.item_id;
|
delete inputPayload.item_id;
|
||||||
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
const decoded = createExceptionListItemSchema.decode(inputPayload);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
tags,
|
tags,
|
||||||
} from '../common/schemas';
|
} from '../common/schemas';
|
||||||
import { Identity, RequiredKeepUndefined } from '../../types';
|
import { Identity, RequiredKeepUndefined } from '../../types';
|
||||||
import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types';
|
import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types';
|
||||||
import { EntriesArray } from '../types/entries';
|
import { EntriesArray } from '../types/entries';
|
||||||
import { DefaultUuid } from '../../siem_common_deps';
|
import { DefaultUuid } from '../../siem_common_deps';
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([
|
||||||
t.exact(
|
t.exact(
|
||||||
t.partial({
|
t.partial({
|
||||||
_tags, // defaults to empty array if not set during decode
|
_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
|
entries: DefaultEntryArray, // defaults to empty array if not set during decode
|
||||||
item_id: DefaultUuid, // defaults to GUID (uuid v4) 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
|
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' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
|
||||||
> & {
|
> & {
|
||||||
_tags: _Tags;
|
_tags: _Tags;
|
||||||
comments: CommentsPartialArray;
|
comments: CreateCommentsArray;
|
||||||
tags: Tags;
|
tags: Tags;
|
||||||
item_id: ItemId;
|
item_id: ItemId;
|
||||||
entries: EntriesArray;
|
entries: EntriesArray;
|
||||||
|
|
|
@ -23,10 +23,10 @@ import {
|
||||||
} from '../common/schemas';
|
} from '../common/schemas';
|
||||||
import { Identity, RequiredKeepUndefined } from '../../types';
|
import { Identity, RequiredKeepUndefined } from '../../types';
|
||||||
import {
|
import {
|
||||||
CommentsPartialArray,
|
|
||||||
DefaultCommentsPartialArray,
|
|
||||||
DefaultEntryArray,
|
DefaultEntryArray,
|
||||||
|
DefaultUpdateCommentsArray,
|
||||||
EntriesArray,
|
EntriesArray,
|
||||||
|
UpdateCommentsArray,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export const updateExceptionListItemSchema = t.intersection([
|
export const updateExceptionListItemSchema = t.intersection([
|
||||||
|
@ -40,7 +40,7 @@ export const updateExceptionListItemSchema = t.intersection([
|
||||||
t.exact(
|
t.exact(
|
||||||
t.partial({
|
t.partial({
|
||||||
_tags, // defaults to empty array if not set during decode
|
_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
|
entries: DefaultEntryArray, // defaults to empty array if not set during decode
|
||||||
id, // defaults to undefined if not set during decode
|
id, // defaults to undefined if not set during decode
|
||||||
item_id: t.union([t.string, t.undefined]),
|
item_id: t.union([t.string, t.undefined]),
|
||||||
|
@ -65,7 +65,7 @@ export type UpdateExceptionListItemSchemaDecoded = Identity<
|
||||||
'_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments'
|
'_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments'
|
||||||
> & {
|
> & {
|
||||||
_tags: _Tags;
|
_tags: _Tags;
|
||||||
comments: CommentsPartialArray;
|
comments: UpdateCommentsArray;
|
||||||
tags: Tags;
|
tags: Tags;
|
||||||
entries: EntriesArray;
|
entries: EntriesArray;
|
||||||
namespace_type: NamespaceType;
|
namespace_type: NamespaceType;
|
||||||
|
|
|
@ -6,17 +6,12 @@
|
||||||
|
|
||||||
import { DATE_NOW, USER } from '../../constants.mock';
|
import { DATE_NOW, USER } from '../../constants.mock';
|
||||||
|
|
||||||
import { CommentsArray } from './comments';
|
import { Comments, CommentsArray } from './comments';
|
||||||
|
|
||||||
export const getCommentsMock = (): CommentsArray => [
|
export const getCommentsMock = (): Comments => ({
|
||||||
{
|
comment: 'some old comment',
|
||||||
comment: 'some comment',
|
created_at: DATE_NOW,
|
||||||
created_at: DATE_NOW,
|
created_by: USER,
|
||||||
created_by: USER,
|
});
|
||||||
},
|
|
||||||
{
|
export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()];
|
||||||
comment: 'some other comment',
|
|
||||||
created_at: DATE_NOW,
|
|
||||||
created_by: 'lily',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
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';
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
export const comment = t.exact(
|
export const comments = t.intersection([
|
||||||
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([
|
|
||||||
t.exact(
|
t.exact(
|
||||||
t.type({
|
t.type({
|
||||||
comment: t.string,
|
comment: t.string,
|
||||||
})
|
|
||||||
),
|
|
||||||
t.exact(
|
|
||||||
t.partial({
|
|
||||||
created_at: t.string, // TODO: Make this into an ISO Date string check,
|
created_at: t.string, // TODO: Make this into an ISO Date string check,
|
||||||
created_by: t.string,
|
created_by: t.string,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
t.exact(
|
||||||
|
t.partial({
|
||||||
|
updated_at: t.string,
|
||||||
|
updated_by: t.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const commentsPartialArray = t.array(commentPartial);
|
export const commentsArray = t.array(comments);
|
||||||
export type CommentsPartialArray = t.TypeOf<typeof commentsPartialArray>;
|
export type CommentsArray = t.TypeOf<typeof commentsArray>;
|
||||||
export type CommentPartial = t.TypeOf<typeof commentPartial>;
|
export type Comments = t.TypeOf<typeof comments>;
|
||||||
export const commentsPartialArrayOrUndefined = t.union([commentsPartialArray, t.undefined]);
|
export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
|
||||||
export type CommentsPartialArrayOrUndefined = t.TypeOf<typeof commentsPartialArrayOrUndefined>;
|
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 * as t from 'io-ts';
|
||||||
import { Either } from 'fp-ts/lib/Either';
|
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 DefaultCommentsArrayC = t.Type<CommentsArray, CommentsArray, unknown>;
|
||||||
export type DefaultCommentsPartialArrayC = t.Type<
|
|
||||||
CommentsPartialArray,
|
|
||||||
CommentsPartialArray,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types the DefaultCommentsArray as:
|
* Types the DefaultCommentsArray as:
|
||||||
|
@ -26,24 +21,8 @@ export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type<
|
||||||
unknown
|
unknown
|
||||||
>(
|
>(
|
||||||
'DefaultCommentsArray',
|
'DefaultCommentsArray',
|
||||||
t.array(comment).is,
|
t.array(comments).is,
|
||||||
(input, context): Either<t.Errors, CommentsArray> =>
|
(input): Either<t.Errors, CommentsArray> =>
|
||||||
input == null ? t.success([]) : t.array(comment).validate(input, context),
|
input == null ? t.success([]) : t.array(comments).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, context): Either<t.Errors, CommentsPartialArray> =>
|
|
||||||
input == null ? t.success([]) : t.array(commentPartial).validate(input, context),
|
|
||||||
t.identity
|
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;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with 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 './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';
|
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
|
// TODO Would like to just use getExceptionListSchemaMock() here, but
|
||||||
// validation returns object in different order, making the strings not match
|
// 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),
|
body: JSON.stringify(payload),
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
signal: abortCtrl.signal,
|
signal: abortCtrl.signal,
|
||||||
|
|
|
@ -176,7 +176,7 @@ export const updateExceptionListItem = async ({
|
||||||
|
|
||||||
if (validatedRequest != null) {
|
if (validatedRequest != null) {
|
||||||
try {
|
try {
|
||||||
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
|
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
|
||||||
body: JSON.stringify(listItem),
|
body: JSON.stringify(listItem),
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
signal,
|
signal,
|
||||||
|
|
|
@ -77,6 +77,12 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
|
||||||
created_by: {
|
created_by: {
|
||||||
type: 'keyword',
|
type: 'keyword',
|
||||||
},
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
updated_by: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entries: {
|
entries: {
|
||||||
|
|
|
@ -5,14 +5,7 @@
|
||||||
"type": "simple",
|
"type": "simple",
|
||||||
"description": "This is a sample change here this list",
|
"description": "This is a sample change here this list",
|
||||||
"name": "Sample Endpoint Exception List update change",
|
"name": "Sample Endpoint Exception List update change",
|
||||||
"comments": [
|
"comments": [{ "comment": "this is a newly added comment" }],
|
||||||
{
|
|
||||||
"comment": "this was an old comment.",
|
|
||||||
"created_by": "lily",
|
|
||||||
"created_at": "2020-04-20T15:25:31.830Z"
|
|
||||||
},
|
|
||||||
{ "comment": "this is a newly added comment" }
|
|
||||||
],
|
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"field": "event.category",
|
"field": "event.category",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommentsPartialArray,
|
CreateCommentsArray,
|
||||||
Description,
|
Description,
|
||||||
EntriesArray,
|
EntriesArray,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
|
@ -25,13 +25,13 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSavedObjectType,
|
getSavedObjectType,
|
||||||
transformComments,
|
transformCreateCommentsToComments,
|
||||||
transformSavedObjectToExceptionListItem,
|
transformSavedObjectToExceptionListItem,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
interface CreateExceptionListItemOptions {
|
interface CreateExceptionListItemOptions {
|
||||||
_tags: _Tags;
|
_tags: _Tags;
|
||||||
comments: CommentsPartialArray;
|
comments: CreateCommentsArray;
|
||||||
listId: ListId;
|
listId: ListId;
|
||||||
itemId: ItemId;
|
itemId: ItemId;
|
||||||
savedObjectsClient: SavedObjectsClientContract;
|
savedObjectsClient: SavedObjectsClientContract;
|
||||||
|
@ -64,9 +64,10 @@ export const createExceptionListItem = async ({
|
||||||
}: CreateExceptionListItemOptions): Promise<ExceptionListItemSchema> => {
|
}: CreateExceptionListItemOptions): Promise<ExceptionListItemSchema> => {
|
||||||
const savedObjectType = getSavedObjectType({ namespaceType });
|
const savedObjectType = getSavedObjectType({ namespaceType });
|
||||||
const dateNow = new Date().toISOString();
|
const dateNow = new Date().toISOString();
|
||||||
|
const transformedComments = transformCreateCommentsToComments({ comments, user });
|
||||||
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
|
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
|
||||||
_tags,
|
_tags,
|
||||||
comments: transformComments({ comments, user }),
|
comments: transformedComments,
|
||||||
created_at: dateNow,
|
created_at: dateNow,
|
||||||
created_by: user,
|
created_by: user,
|
||||||
description,
|
description,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { SavedObjectsClientContract } from 'kibana/server';
|
import { SavedObjectsClientContract } from 'kibana/server';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommentsPartialArray,
|
CreateCommentsArray,
|
||||||
Description,
|
Description,
|
||||||
DescriptionOrUndefined,
|
DescriptionOrUndefined,
|
||||||
EntriesArray,
|
EntriesArray,
|
||||||
|
@ -30,6 +30,7 @@ import {
|
||||||
SortOrderOrUndefined,
|
SortOrderOrUndefined,
|
||||||
Tags,
|
Tags,
|
||||||
TagsOrUndefined,
|
TagsOrUndefined,
|
||||||
|
UpdateCommentsArray,
|
||||||
_Tags,
|
_Tags,
|
||||||
_TagsOrUndefined,
|
_TagsOrUndefined,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
|
@ -88,7 +89,7 @@ export interface GetExceptionListItemOptions {
|
||||||
|
|
||||||
export interface CreateExceptionListItemOptions {
|
export interface CreateExceptionListItemOptions {
|
||||||
_tags: _Tags;
|
_tags: _Tags;
|
||||||
comments: CommentsPartialArray;
|
comments: CreateCommentsArray;
|
||||||
entries: EntriesArray;
|
entries: EntriesArray;
|
||||||
itemId: ItemId;
|
itemId: ItemId;
|
||||||
listId: ListId;
|
listId: ListId;
|
||||||
|
@ -102,7 +103,7 @@ export interface CreateExceptionListItemOptions {
|
||||||
|
|
||||||
export interface UpdateExceptionListItemOptions {
|
export interface UpdateExceptionListItemOptions {
|
||||||
_tags: _TagsOrUndefined;
|
_tags: _TagsOrUndefined;
|
||||||
comments: CommentsPartialArray;
|
comments: UpdateCommentsArray;
|
||||||
entries: EntriesArrayOrUndefined;
|
entries: EntriesArrayOrUndefined;
|
||||||
id: IdOrUndefined;
|
id: IdOrUndefined;
|
||||||
itemId: ItemIdOrUndefined;
|
itemId: ItemIdOrUndefined;
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { SavedObjectsClientContract } from 'kibana/server';
|
import { SavedObjectsClientContract } from 'kibana/server';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommentsPartialArray,
|
|
||||||
DescriptionOrUndefined,
|
DescriptionOrUndefined,
|
||||||
EntriesArrayOrUndefined,
|
EntriesArrayOrUndefined,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
|
@ -19,19 +18,20 @@ import {
|
||||||
NameOrUndefined,
|
NameOrUndefined,
|
||||||
NamespaceType,
|
NamespaceType,
|
||||||
TagsOrUndefined,
|
TagsOrUndefined,
|
||||||
|
UpdateCommentsArrayOrUndefined,
|
||||||
_TagsOrUndefined,
|
_TagsOrUndefined,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSavedObjectType,
|
getSavedObjectType,
|
||||||
transformComments,
|
|
||||||
transformSavedObjectUpdateToExceptionListItem,
|
transformSavedObjectUpdateToExceptionListItem,
|
||||||
|
transformUpdateCommentsToComments,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { getExceptionListItem } from './get_exception_list_item';
|
import { getExceptionListItem } from './get_exception_list_item';
|
||||||
|
|
||||||
interface UpdateExceptionListItemOptions {
|
interface UpdateExceptionListItemOptions {
|
||||||
id: IdOrUndefined;
|
id: IdOrUndefined;
|
||||||
comments: CommentsPartialArray;
|
comments: UpdateCommentsArrayOrUndefined;
|
||||||
_tags: _TagsOrUndefined;
|
_tags: _TagsOrUndefined;
|
||||||
name: NameOrUndefined;
|
name: NameOrUndefined;
|
||||||
description: DescriptionOrUndefined;
|
description: DescriptionOrUndefined;
|
||||||
|
@ -71,12 +71,17 @@ export const updateExceptionListItem = async ({
|
||||||
if (exceptionListItem == null) {
|
if (exceptionListItem == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
const transformedComments = transformUpdateCommentsToComments({
|
||||||
|
comments,
|
||||||
|
existingComments: exceptionListItem.comments,
|
||||||
|
user,
|
||||||
|
});
|
||||||
const savedObject = await savedObjectsClient.update<ExceptionListSoSchema>(
|
const savedObject = await savedObjectsClient.update<ExceptionListSoSchema>(
|
||||||
savedObjectType,
|
savedObjectType,
|
||||||
exceptionListItem.id,
|
exceptionListItem.id,
|
||||||
{
|
{
|
||||||
_tags,
|
_tags,
|
||||||
comments: transformComments({ comments, user }),
|
comments: transformedComments,
|
||||||
description,
|
description,
|
||||||
entries,
|
entries,
|
||||||
meta,
|
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 { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
|
||||||
|
|
||||||
|
import { ErrorWithStatusCode } from '../../error_with_status_code';
|
||||||
import {
|
import {
|
||||||
|
Comments,
|
||||||
|
CommentsArray,
|
||||||
CommentsArrayOrUndefined,
|
CommentsArrayOrUndefined,
|
||||||
CommentsPartialArrayOrUndefined,
|
CreateComments,
|
||||||
|
CreateCommentsArrayOrUndefined,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
ExceptionListSchema,
|
ExceptionListSchema,
|
||||||
ExceptionListSoSchema,
|
ExceptionListSoSchema,
|
||||||
FoundExceptionListItemSchema,
|
FoundExceptionListItemSchema,
|
||||||
FoundExceptionListSchema,
|
FoundExceptionListSchema,
|
||||||
NamespaceType,
|
NamespaceType,
|
||||||
|
UpdateCommentsArrayOrUndefined,
|
||||||
|
comments as commentsSchema,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
import {
|
import {
|
||||||
SavedObjectType,
|
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,
|
comments,
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
comments: CommentsPartialArrayOrUndefined;
|
comments: CreateCommentsArrayOrUndefined;
|
||||||
user: string;
|
user: string;
|
||||||
}): CommentsArrayOrUndefined => {
|
}): CommentsArrayOrUndefined => {
|
||||||
const dateNow = new Date().toISOString();
|
const dateNow = new Date().toISOString();
|
||||||
if (comments != null) {
|
if (comments != null) {
|
||||||
return comments.map((comment) => {
|
return comments.map((c: CreateComments) => {
|
||||||
return {
|
if (c.comment.trim().length === 0) {
|
||||||
comment: comment.comment,
|
throw new ErrorWithStatusCode('Empty comments not allowed', 403);
|
||||||
created_at: comment.created_at ?? dateNow,
|
} else {
|
||||||
created_by: comment.created_by ?? user,
|
return {
|
||||||
};
|
comment: c.comment,
|
||||||
|
created_at: dateNow,
|
||||||
|
created_by: user,
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return comments;
|
return comments;
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
getEntryMatchAnyMock,
|
getEntryMatchAnyMock,
|
||||||
getEntriesArrayMock,
|
getEntriesArrayMock,
|
||||||
} from '../../../../../lists/common/schemas/types/entries.mock';
|
} 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', () => {
|
describe('Exception helpers', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -382,7 +382,7 @@ describe('Exception helpers', () => {
|
||||||
|
|
||||||
describe('#getFormattedComments', () => {
|
describe('#getFormattedComments', () => {
|
||||||
test('it returns formatted comment object with username and timestamp', () => {
|
test('it returns formatted comment object with username and timestamp', () => {
|
||||||
const payload = getCommentsMock();
|
const payload = getCommentsArrayMock();
|
||||||
const result = getFormattedComments(payload);
|
const result = getFormattedComments(payload);
|
||||||
|
|
||||||
expect(result[0].username).toEqual('some user');
|
expect(result[0].username).toEqual('some user');
|
||||||
|
@ -390,7 +390,7 @@ describe('Exception helpers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it returns formatted timeline icon with comment users initial', () => {
|
test('it returns formatted timeline icon with comment users initial', () => {
|
||||||
const payload = getCommentsMock();
|
const payload = getCommentsArrayMock();
|
||||||
const result = getFormattedComments(payload);
|
const result = getFormattedComments(payload);
|
||||||
|
|
||||||
const wrapper = mount<React.ReactElement>(result[0].timelineIcon as React.ReactElement);
|
const wrapper = mount<React.ReactElement>(result[0].timelineIcon as React.ReactElement);
|
||||||
|
@ -399,12 +399,12 @@ describe('Exception helpers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it returns comment text', () => {
|
test('it returns comment text', () => {
|
||||||
const payload = getCommentsMock();
|
const payload = getCommentsArrayMock();
|
||||||
const result = getFormattedComments(payload);
|
const result = getFormattedComments(payload);
|
||||||
|
|
||||||
const wrapper = mount<React.ReactElement>(result[0].children as React.ReactElement);
|
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 moment from 'moment';
|
||||||
|
|
||||||
import * as i18n from './translations';
|
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 { EXCEPTION_OPERATORS, isOperator } from './operators';
|
||||||
import {
|
import {
|
||||||
|
CommentsArray,
|
||||||
Entry,
|
Entry,
|
||||||
EntriesArray,
|
EntriesArray,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
|
@ -183,7 +184,7 @@ export const getDescriptionListContent = (
|
||||||
*
|
*
|
||||||
* @param comments ExceptionItem.comments
|
* @param comments ExceptionItem.comments
|
||||||
*/
|
*/
|
||||||
export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] =>
|
export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] =>
|
||||||
comments.map((comment) => ({
|
comments.map((comment) => ({
|
||||||
username: comment.created_by,
|
username: comment.created_by,
|
||||||
timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'),
|
timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'),
|
||||||
|
|
|
@ -26,12 +26,6 @@ export interface DescriptionListItem {
|
||||||
description: NonNullable<ReactNode>;
|
description: NonNullable<ReactNode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
|
||||||
created_by: string;
|
|
||||||
created_at: string;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ExceptionListType {
|
export enum ExceptionListType {
|
||||||
DETECTION_ENGINE = 'detection',
|
DETECTION_ENGINE = 'detection',
|
||||||
ENDPOINT = 'endpoint',
|
ENDPOINT = 'endpoint',
|
||||||
|
|
|
@ -12,7 +12,7 @@ import moment from 'moment-timezone';
|
||||||
|
|
||||||
import { ExceptionDetails } from './exception_details';
|
import { ExceptionDetails } from './exception_details';
|
||||||
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
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', () => {
|
describe('ExceptionDetails', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -42,7 +42,7 @@ describe('ExceptionDetails', () => {
|
||||||
|
|
||||||
test('it renders comments button if comments exist', () => {
|
test('it renders comments button if comments exist', () => {
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
@ -60,7 +60,7 @@ describe('ExceptionDetails', () => {
|
||||||
|
|
||||||
test('it renders correct number of comments', () => {
|
test('it renders correct number of comments', () => {
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = [getCommentsMock()[0]];
|
exceptionItem.comments = [getCommentsArrayMock()[0]];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
@ -78,7 +78,7 @@ describe('ExceptionDetails', () => {
|
||||||
|
|
||||||
test('it renders comments plural if more than one', () => {
|
test('it renders comments plural if more than one', () => {
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
@ -96,7 +96,7 @@ describe('ExceptionDetails', () => {
|
||||||
|
|
||||||
test('it renders comments show text if "showComments" is false', () => {
|
test('it renders comments show text if "showComments" is false', () => {
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
@ -114,7 +114,7 @@ describe('ExceptionDetails', () => {
|
||||||
|
|
||||||
test('it renders comments hide text if "showComments" is true', () => {
|
test('it renders comments hide text if "showComments" is true', () => {
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
@ -133,7 +133,7 @@ describe('ExceptionDetails', () => {
|
||||||
test('it invokes "onCommentsClick" when comments button clicked', () => {
|
test('it invokes "onCommentsClick" when comments button clicked', () => {
|
||||||
const mockOnCommentsClick = jest.fn();
|
const mockOnCommentsClick = jest.fn();
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionDetails
|
<ExceptionDetails
|
||||||
|
|
|
@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
|
|
||||||
import { ExceptionItem } from './';
|
import { ExceptionItem } from './';
|
||||||
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
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) => (
|
addDecorator((storyFn) => (
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
||||||
|
@ -68,7 +68,7 @@ storiesOf('Components|ExceptionItem', module)
|
||||||
const payload = getExceptionListItemSchemaMock();
|
const payload = getExceptionListItemSchemaMock();
|
||||||
payload._tags = [];
|
payload._tags = [];
|
||||||
payload.description = '';
|
payload.description = '';
|
||||||
payload.comments = getCommentsMock();
|
payload.comments = getCommentsArrayMock();
|
||||||
payload.entries = [
|
payload.entries = [
|
||||||
{
|
{
|
||||||
field: 'actingProcess.file.signer',
|
field: 'actingProcess.file.signer',
|
||||||
|
@ -106,7 +106,7 @@ storiesOf('Components|ExceptionItem', module)
|
||||||
})
|
})
|
||||||
.add('with everything', () => {
|
.add('with everything', () => {
|
||||||
const payload = getExceptionListItemSchemaMock();
|
const payload = getExceptionListItemSchemaMock();
|
||||||
payload.comments = getCommentsMock();
|
payload.comments = getCommentsArrayMock();
|
||||||
return (
|
return (
|
||||||
<ExceptionItem
|
<ExceptionItem
|
||||||
loadingItemIds={[]}
|
loadingItemIds={[]}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
|
|
||||||
import { ExceptionItem } from './';
|
import { ExceptionItem } from './';
|
||||||
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
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', () => {
|
describe('ExceptionItem', () => {
|
||||||
it('it renders ExceptionDetails and ExceptionEntries', () => {
|
it('it renders ExceptionDetails and ExceptionEntries', () => {
|
||||||
|
@ -83,7 +83,7 @@ describe('ExceptionItem', () => {
|
||||||
it('it renders comment accordion closed to begin with', () => {
|
it('it renders comment accordion closed to begin with', () => {
|
||||||
const mockOnDeleteException = jest.fn();
|
const mockOnDeleteException = jest.fn();
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionItem
|
<ExceptionItem
|
||||||
|
@ -102,7 +102,7 @@ describe('ExceptionItem', () => {
|
||||||
it('it renders comment accordion open when showComments is true', () => {
|
it('it renders comment accordion open when showComments is true', () => {
|
||||||
const mockOnDeleteException = jest.fn();
|
const mockOnDeleteException = jest.fn();
|
||||||
const exceptionItem = getExceptionListItemSchemaMock();
|
const exceptionItem = getExceptionListItemSchemaMock();
|
||||||
exceptionItem.comments = getCommentsMock();
|
exceptionItem.comments = getCommentsArrayMock();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||||
<ExceptionItem
|
<ExceptionItem
|
||||||
|
|
|
@ -15,6 +15,7 @@ export {
|
||||||
UseExceptionListSuccess,
|
UseExceptionListSuccess,
|
||||||
} from '../../lists/public';
|
} from '../../lists/public';
|
||||||
export {
|
export {
|
||||||
|
CommentsArray,
|
||||||
ExceptionListSchema,
|
ExceptionListSchema,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
Entry,
|
Entry,
|
||||||
|
|
Loading…
Reference in a new issue