From 59af44bf174d4c00e6f3713e899c254359efbc9b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 27 Oct 2020 12:11:22 -0600 Subject: [PATCH 01/11] [Security Solution] critical pref bug with browser fields reducer ## Summary How to test this performance issue? Ensure that you add extra mappings of mapping-test-1k-block-1, 2, 3,4, etc.. that I have included within your advanced settings like below to a Kibana space: ```ts apm-*-transaction*, auditbeat-*, endgame-*, filebeat-*, logs-*, packetbeat-*, winlogbeat-*, mapping-test-1k-block-1, mapping-test-1k-block-2, mapping-test-1k-block-3, mapping-test-1k-block-4 ``` Screen Shot 2020-10-26 at 11 35 12 AM Modify these lines to add manual perf lines like so for the before numbers: file: x-pack/plugins/security_solution/public/common/containers/source/index.tsx ```ts export const getBrowserFields = memoizeOne( (_title: string, fields: IndexField[]): BrowserFields => { console.time('getBrowserFields'); // Start time const value = fields && fields.length > 0 ? fields.reduce( (accumulator: BrowserFields, field: IndexField) => set([field.category, 'fields', field.name], field, accumulator), {} ) : {}; console.timeEnd('getBrowserFields'); // End time return value; }, // Update the value only if _title has changed (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); ``` Then load any of the SIEM webpages or create a detection engine rule and change around your input indexes. Perf of this before the fix is going to show up in your dev tools console output like this: ```ts getBrowserFields: 7875.5009765625 ms ``` Now, checkout this branch and change the code to be like so for the perf test of the mutatious version for after: file: x-pack/plugins/security_solution/public/common/containers/source/index.tsx ```ts /** * HOT Code path where the fields can be 16087 in length or larger. This is * VERY mutatious on purpose to improve the performance of the transform. */ export const getBrowserFields = memoizeOne( (_title: string, fields: IndexField[]): BrowserFields => { console.time('getBrowserFields'); // Start time // Adds two dangerous casts to allow for mutations within this function type DangerCastForMutation = Record; type DangerCastForBrowserFieldsMutation = Record< string, Omit & { fields: Record } >; // We mutate this instead of using lodash/set to keep this as fast as possible const value = fields.reduce((accumulator, field) => { if (accumulator[field.category] == null) { (accumulator as DangerCastForMutation)[field.category] = {}; } if (accumulator[field.category].fields == null) { accumulator[field.category].fields = {}; } if (accumulator[field.category].fields[field.name] == null) { (accumulator[field.category].fields as DangerCastForMutation)[field.name] = {}; } accumulator[field.category].fields[field.name] = (field as unknown) as BrowserField; return accumulator; }, {}); console.timeEnd('getBrowserFields'); // End time return value; }, // Update the value only if _title has changed (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); ``` Re-load any and all pages and you should see this now: ```ts getBrowserFields: 11.258056640625 ms ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../common/containers/source/index.test.tsx | 30 +++++++++++++++++ .../public/common/containers/source/index.tsx | 33 ++++++++++++++----- 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx new file mode 100644 index 0000000000000..e81b52f281519 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { IndexField } from '../../../../common/search_strategy/index_fields'; +import { getBrowserFields } from '.'; +import { mockBrowserFields, mocksSource } from './mock'; + +describe('source/index.tsx', () => { + describe('getBrowserFields', () => { + test('it returns an empty object given an empty array', () => { + const fields = getBrowserFields('title 1', []); + expect(fields).toEqual({}); + }); + + test('it returns the same input with the same title', () => { + getBrowserFields('title 1', []); + // Since it is memoized it will return the same output which is empty object given 'title 1' a second time + const fields = getBrowserFields('title 1', mocksSource.indexFields as IndexField[]); + expect(fields).toEqual({}); + }); + + test('it transforms input into output as expected', () => { + const fields = getBrowserFields('title 2', mocksSource.indexFields as IndexField[]); + expect(fields).toEqual(mockBrowserFields); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 2cc1c75015e07..47e550b2ced0f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from '@elastic/safer-lodash-set/fp'; import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -55,15 +54,31 @@ export const getIndexFields = memoizeOne( (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length ); +/** + * HOT Code path where the fields can be 16087 in length or larger. This is + * VERY mutatious on purpose to improve the performance of the transform. + */ export const getBrowserFields = memoizeOne( - (_title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {}, + (_title: string, fields: IndexField[]): BrowserFields => { + // Adds two dangerous casts to allow for mutations within this function + type DangerCastForMutation = Record; + type DangerCastForBrowserFieldsMutation = Record< + string, + Omit & { fields: Record } + >; + + // We mutate this instead of using lodash/set to keep this as fast as possible + return fields.reduce((accumulator, field) => { + if (accumulator[field.category] == null) { + (accumulator as DangerCastForMutation)[field.category] = {}; + } + if (accumulator[field.category].fields == null) { + accumulator[field.category].fields = {}; + } + accumulator[field.category].fields[field.name] = (field as unknown) as BrowserField; + return accumulator; + }, {}); + }, // Update the value only if _title has changed (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); From 1066602ab7c4d52139c6156601f182da653dd80c Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 27 Oct 2020 12:28:33 -0600 Subject: [PATCH 02/11] [Security Solutions][Detection Engine] Changes wording for threat matches and rules (#81334) ## Summary Changes the wording for threat matches and rules cc @marrasherrier @MikePaquette @paulewing Before: Screen Shot 2020-10-21 at 8 52 44 AM After: Screen Shot 2020-10-26 at 10 10 17 PM ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) --- .../public/common/components/threat_match/translations.ts | 2 +- .../components/rules/description_step/helpers.test.tsx | 2 +- .../components/rules/description_step/translations.tsx | 2 +- .../components/rules/select_rule_type/translations.ts | 5 +++-- .../detections/components/rules/step_define_rule/schema.tsx | 6 +++--- .../scripts/rules/queries/query_with_threat_mapping.json | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts index ca9f6a13856cf..57e7416731486 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts @@ -13,7 +13,7 @@ export const FIELD = i18n.translate('xpack.securitySolution.threatMatch.fieldDes export const THREAT_FIELD = i18n.translate( 'xpack.securitySolution.threatMatch.threatFieldDescription', { - defaultMessage: 'Threat index field', + defaultMessage: 'Indicator index field', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index ebdfdcc262b34..ee1edecbdc54a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -437,7 +437,7 @@ describe('helpers', () => { it('returns a humanized description for a threat_match type', () => { const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threat_match'); - expect(result.description).toEqual('Threat Match'); + expect(result.description).toEqual('Indicator Match'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index d9186c2da7225..04647871f212e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -65,7 +65,7 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription', { - defaultMessage: 'Threat Match', + defaultMessage: 'Indicator Match', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts index 7043aa2d2f956..b9c229fe78f10 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts @@ -66,13 +66,14 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( export const THREAT_MATCH_TYPE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle', { - defaultMessage: 'Threat Match', + defaultMessage: 'Indicator Match', } ); export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchDescription', { - defaultMessage: 'Upload value lists to write rules around a list of known bad attributes', + defaultMessage: + 'Use indicators from intelligence sources to detect matching events and alerts.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index ebffb1abf4787..9763125776be2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -235,7 +235,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel', { - defaultMessage: 'Threat index patterns', + defaultMessage: 'Indicator Index Patterns', } ), helpText: {THREAT_MATCH_INDEX_HELPER_TEXT}, @@ -265,7 +265,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel', { - defaultMessage: 'Threat Mapping', + defaultMessage: 'Indicator Mapping', } ), validations: [ @@ -301,7 +301,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel', { - defaultMessage: 'Threat index query', + defaultMessage: 'Indicator Index Query', } ), validations: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json index 1e2f217751e96..ed9356f46501c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json @@ -1,5 +1,5 @@ { - "name": "Query with a threat mapping", + "name": "Query with a indicator mapping", "description": "Query with a threat mapping", "rule_id": "threat-mapping", "risk_score": 1, From 30a0323d7a708c56c9162d05f5fa6df325ba3afd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 21:01:36 +0200 Subject: [PATCH 03/11] [Seurity Solution][Case] Create case plugin client (#81018) --- .../case/server/client/cases/create.test.ts | 335 +++++++++++++++ .../case/server/client/cases/create.ts | 78 ++++ .../case/server/client/cases/update.test.ts | 383 ++++++++++++++++++ .../case/server/client/cases/update.ts | 155 +++++++ .../case/server/client/comments/add.test.ts | 212 ++++++++++ .../case/server/client/comments/add.ts | 126 ++++++ .../plugins/case/server/client/index.test.ts | 68 ++++ x-pack/plugins/case/server/client/index.ts | 44 ++ x-pack/plugins/case/server/client/mocks.ts | 53 +++ x-pack/plugins/case/server/client/types.ts | 46 +++ x-pack/plugins/case/server/plugin.ts | 93 ++++- .../__fixtures__/create_mock_so_repository.ts | 10 +- .../api/__fixtures__/mock_saved_objects.ts | 11 +- .../routes/api/__fixtures__/route_contexts.ts | 30 +- .../api/cases/comments/delete_comment.test.ts | 4 +- .../api/cases/comments/get_comment.test.ts | 4 +- .../api/cases/comments/patch_comment.test.ts | 6 +- .../api/cases/comments/post_comment.test.ts | 15 +- .../routes/api/cases/comments/post_comment.ts | 118 +----- .../api/cases/configure/get_configure.test.ts | 8 +- .../cases/configure/get_connectors.test.ts | 4 +- .../cases/configure/patch_configure.test.ts | 12 +- .../cases/configure/post_configure.test.ts | 32 +- .../routes/api/cases/delete_cases.test.ts | 8 +- .../routes/api/cases/find_cases.test.ts | 8 +- .../server/routes/api/cases/get_case.test.ts | 14 +- .../routes/api/cases/patch_cases.test.ts | 16 +- .../server/routes/api/cases/patch_cases.ts | 150 +------ .../server/routes/api/cases/post_case.test.ts | 13 +- .../case/server/routes/api/cases/post_case.ts | 70 +--- .../server/routes/api/cases/push_case.test.ts | 6 +- .../plugins/case/server/routes/api/utils.ts | 2 +- x-pack/plugins/case/server/services/index.ts | 2 +- x-pack/plugins/case/server/services/mocks.ts | 43 ++ x-pack/plugins/case/server/types.ts | 17 + 35 files changed, 1798 insertions(+), 398 deletions(-) create mode 100644 x-pack/plugins/case/server/client/cases/create.test.ts create mode 100644 x-pack/plugins/case/server/client/cases/create.ts create mode 100644 x-pack/plugins/case/server/client/cases/update.test.ts create mode 100644 x-pack/plugins/case/server/client/cases/update.ts create mode 100644 x-pack/plugins/case/server/client/comments/add.test.ts create mode 100644 x-pack/plugins/case/server/client/comments/add.ts create mode 100644 x-pack/plugins/case/server/client/index.test.ts create mode 100644 x-pack/plugins/case/server/client/index.ts create mode 100644 x-pack/plugins/case/server/client/mocks.ts create mode 100644 x-pack/plugins/case/server/client/types.ts create mode 100644 x-pack/plugins/case/server/services/mocks.ts create mode 100644 x-pack/plugins/case/server/types.ts diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts new file mode 100644 index 0000000000000..f253dd9f4feb4 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -0,0 +1,335 @@ +/* + * 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 { ConnectorTypes, CasePostRequest } from '../../../common/api'; + +import { + createMockSavedObjectsRepository, + mockCaseConfigure, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('create', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it creates the case correctly', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + } as CasePostRequest; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'Awesome D00d', email: 'd00d@awesome.com', username: 'awesome' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'create', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['description', 'status', 'tags', 'title', 'connector'], + new_value: + '{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}}}', + old_value: null, + }, + references: [ + { + id: 'mock-it', + name: 'associated-cases', + type: 'cases', + }, + ], + }, + ]); + }); + + test('it creates the case without connector in the configuration', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'Awesome D00d', email: 'd00d@awesome.com', username: 'awesome' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + + test('Allow user to create case without authentication', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing title', async () => { + expect.assertions(1); + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing description', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing tags', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing connector ', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when connector missing the right fields', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: {}, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws if you passing status for a new case', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + status: 'closed', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + }); + + it(`Returns an error if postNewCase throws`, async () => { + const postCase = { + description: 'Throw an error', + title: 'Super Bad Security Issue', + tags: ['error'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + + caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts new file mode 100644 index 0000000000000..3379099419a75 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -0,0 +1,78 @@ +/* + * 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 Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; + +import { + CasePostRequestRt, + throwErrors, + excess, + CaseResponseRt, + CaseResponse, +} from '../../../common/api'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; +import { + getConnectorFromConfiguration, + transformCaseConnectorToEsConnector, +} from '../../routes/api/cases/helpers'; + +import { CaseClientCreate, CaseClientFactoryArguments } from '../types'; + +export const create = ({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ theCase }: CaseClientCreate): Promise => { + const query = pipe( + excess(CasePostRequestRt).decode(theCase), + fold(throwErrors(Boom.badRequest), identity) + ); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const createdDate = new Date().toISOString(); + const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); + + const newCase = await caseService.postNewCase({ + client: savedObjectsClient, + attributes: transformNewCase({ + createdDate, + newCase: query, + username, + full_name, + email, + connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), + }), + }); + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCaseUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: newCase.id, + fields: ['description', 'status', 'tags', 'title', 'connector'], + newValue: JSON.stringify(query), + }), + ], + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: newCase, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts new file mode 100644 index 0000000000000..62d897999c11a --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -0,0 +1,383 @@ +/* + * 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 { ConnectorTypes, CasesPatchRequest } from '../../../common/api'; +import { + createMockSavedObjectsRepository, + mockCaseNoConnectorId, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('update', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it closes the case correctly', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'closed' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + comments: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + id: 'mock-id-1', + external_service: null, + status: 'closed', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'update', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['status'], + new_value: 'closed', + old_value: 'open', + }, + references: [ + { + id: 'mock-id-1', + name: 'associated-cases', + type: 'cases', + }, + ], + }, + ]); + }); + + test('it opens the case correctly', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'open' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } }, + ...mockCases.slice(1), + ], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + closed_at: null, + closed_by: null, + comments: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + id: 'mock-id-1', + external_service: null, + status: 'open', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + + test('it updates a case without a connector.id', async () => { + const patchCases = { + cases: [ + { + id: 'mock-no-connector_id', + status: 'closed' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [mockCaseNoConnectorId], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + id: 'mock-no-connector_id', + comments: [], + totalComment: 0, + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'elastic', email: 'testemail@elastic.co', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'closed', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + + test('it updates the connector correctly', async () => { + const patchCases = ({ + cases: [ + { + id: 'mock-id-3', + connector: { + id: '456', + name: 'My connector 2', + type: ConnectorTypes.jira, + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }, + version: 'WzUsMV0=', + }, + ], + } as unknown) as CasesPatchRequest; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + id: 'mock-id-3', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { + id: '456', + name: 'My connector 2', + type: ConnectorTypes.jira, + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }, + created_at: '2019-11-25T22:32:17.947Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + external_service: null, + title: 'Another bad one', + status: 'open', + tags: ['LOLBins'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'Awesome D00d', + email: 'd00d@awesome.com', + username: 'awesome', + }, + version: 'WzE3LDFd', + }, + ]); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing id', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing version', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-3', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when fields are identical', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'open' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe('All update fields are identical to current version.') + ); + }); + + test('it throws when case does not exist', async () => { + const patchCases = { + cases: [ + { + id: 'not-exists', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe( + 'These cases not-exists do not exist. Please check you have the correct ids.' + ) + ); + }); + + test('it throws when cases conflicts', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV1=', + title: 'Super Bad Security Issue', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe( + 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' + ) + ); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts new file mode 100644 index 0000000000000..424f51ee40f08 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -0,0 +1,155 @@ +/* + * 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 Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject } from '../../routes/api/utils'; + +import { + throwErrors, + excess, + CasesResponseRt, + CasesPatchRequestRt, + ESCasePatchRequest, + CasePatchRequest, + CasesResponse, +} from '../../../common/api'; +import { buildCaseUserActions } from '../../services/user_actions/helpers'; +import { + getCaseToUpdate, + transformCaseConnectorToEsConnector, +} from '../../routes/api/cases/helpers'; + +import { CaseClientUpdate, CaseClientFactoryArguments } from '../types'; + +export const update = ({ + savedObjectsClient, + caseService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ cases }: CaseClientUpdate): Promise => { + const query = pipe( + excess(CasesPatchRequestRt).decode(cases), + fold(throwErrors(Boom.badRequest), identity) + ); + + const myCases = await caseService.getCases({ + client: savedObjectsClient, + caseIds: query.cases.map((q) => q.id), + }); + + let nonExistingCases: CasePatchRequest[] = []; + const conflictedCases = query.cases.filter((q) => { + const myCase = myCases.saved_objects.find((c) => c.id === q.id); + + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } + return myCase == null || myCase?.version !== q.version; + }); + + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + + if (conflictedCases.length > 0) { + throw Boom.conflict( + `These cases ${conflictedCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } + + const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); + const { connector, ...thisCase } = updateCase; + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...thisCase, + ...(connector != null + ? { connector: transformCaseConnectorToEsConnector(connector) } + : {}), + }) + : { id: thisCase.id, version: thisCase.version }; + }); + + const updateFilterCases = updateCases.filter((updateCase) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); + + if (updateFilterCases.length > 0) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const updatedDt = new Date().toISOString(); + const updatedCases = await caseService.patchCases({ + client: savedObjectsClient, + cases: updateFilterCases.map((thisCase) => { + const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + caseId, + updatedAttributes: { + ...updateCaseAttributes, + ...closedInfo, + updated_at: updatedDt, + updated_by: { email, full_name, username }, + }, + version, + }; + }), + }); + + const returnUpdatedCase = myCases.saved_objects + .filter((myCase) => + updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) + ) + .map((myCase) => { + const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); + return flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + version: updatedCase?.version ?? myCase.version, + }, + }); + }); + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: buildCaseUserActions({ + originalCases: myCases.saved_objects, + updatedCases: updatedCases.saved_objects, + actionDate: updatedDt, + actionBy: { email, full_name, username }, + }), + }); + + return CasesResponseRt.encode(returnUpdatedCase); + } + throw Boom.notAcceptable('All update fields are identical to current version.'); +}; diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts new file mode 100644 index 0000000000000..8a316740e41e0 --- /dev/null +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -0,0 +1,212 @@ +/* + * 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 { + createMockSavedObjectsRepository, + mockCaseComments, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('addComment', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-10-23T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it adds a comment correctly', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.id).toEqual('mock-id-1'); + expect(res.totalComment).toEqual(res.comments!.length); + expect(res.comments![res.comments!.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + + test('it updates the case correctly after adding a comment', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); + expect(res.updated_by).toEqual({ + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }); + }); + + test('it creates a user action', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'create', + action_at: '2020-10-23T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['comment'], + new_value: 'Wow, good luck catching that bad meanie!', + old_value: null, + }, + references: [ + { + id: 'mock-id-1', + name: 'associated-cases', + type: 'cases', + }, + { + id: 'mock-comment', + name: 'associated-cases-comments', + type: 'cases-comments', + }, + ], + }, + ]); + }); + + test('it allow user to create comments without authentications', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.id).toEqual('mock-id-1'); + expect(res.comments![res.comments!.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing comment', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + // @ts-expect-error + comment: {}, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + + test('it throws when the case does not exists', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'not-exists', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(404); + }); + }); + + test('it throws when postNewCase throws', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Throw an error' }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts new file mode 100644 index 0000000000000..765eb2c873765 --- /dev/null +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; + +import { + throwErrors, + excess, + CaseResponseRt, + CommentRequestRt, + CaseResponse, +} from '../../../common/api'; +import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; + +import { CaseClientAddComment, CaseClientFactoryArguments } from '../types'; +import { CASE_SAVED_OBJECT } from '../../saved_object_types'; + +export const addComment = ({ + savedObjectsClient, + caseService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ + caseId, + comment, +}: CaseClientAddComment): Promise => { + const query = pipe( + excess(CommentRequestRt).decode(comment), + fold(throwErrors(Boom.badRequest), identity) + ); + + const myCase = await caseService.getCase({ + client: savedObjectsClient, + caseId, + }); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const createdDate = new Date().toISOString(); + + const [newComment, updatedCase] = await Promise.all([ + caseService.postNewComment({ + client: savedObjectsClient, + attributes: transformNewComment({ + createdDate, + ...query, + username, + full_name, + email, + }), + references: [ + { + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + id: myCase.id, + }, + ], + }), + caseService.patchCase({ + client: savedObjectsClient, + caseId, + updatedAttributes: { + updated_at: createdDate, + updated_by: { username, full_name, email }, + }, + version: myCase.version, + }), + ]); + + const totalCommentsFindByCases = await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: 1, + }, + }); + + const [comments] = await Promise.all([ + caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: totalCommentsFindByCases.total, + }, + }), + userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: myCase.id, + commentId: newComment.id, + fields: ['comment'], + newValue: query.comment, + }), + ], + }), + ]); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase.attributes }, + version: updatedCase.version ?? myCase.version, + references: myCase.references, + }, + comments: comments.saved_objects, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts new file mode 100644 index 0000000000000..1ecdc8ea96dea --- /dev/null +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { createCaseClient } from '.'; +import { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../services/mocks'; + +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add'; + +jest.mock('./cases/create'); +jest.mock('./cases/update'); +jest.mock('./comments/add'); + +const caseService = createCaseServiceMock(); +const caseConfigureService = createConfigureServiceMock(); +const userActionService = createUserActionServiceMock(); +const savedObjectsClient = savedObjectsClientMock.create(); +const request = {} as KibanaRequest; + +const createMock = create as jest.Mock; +const updateMock = update as jest.Mock; +const addCommentMock = addComment as jest.Mock; + +describe('createCaseClient()', () => { + test('it creates the client correctly', async () => { + createCaseClient({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(createMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(updateMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(addCommentMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts new file mode 100644 index 0000000000000..75e9e3c4cfebc --- /dev/null +++ b/x-pack/plugins/case/server/client/index.ts @@ -0,0 +1,44 @@ +/* + * 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 { CaseClientFactoryArguments, CaseClient } from './types'; +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add'; + +export { CaseClient } from './types'; + +export const createCaseClient = ({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, +}: CaseClientFactoryArguments): CaseClient => { + return { + create: create({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + update: update({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + addComment: addComment({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + }; +}; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts new file mode 100644 index 0000000000000..243dd884f9ef6 --- /dev/null +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -0,0 +1,53 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { CaseService, CaseConfigureService, CaseUserActionServiceSetup } from '../services'; +import { CaseClient } from './types'; +import { authenticationMock } from '../routes/api/__fixtures__'; +import { createCaseClient } from '.'; + +export type CaseClientMock = jest.Mocked; +export const createCaseClientMock = (): CaseClientMock => ({ + create: jest.fn(), + update: jest.fn(), + addComment: jest.fn(), +}); + +export const createCaseClientWithMockSavedObjectsClient = async ( + savedObjectsClient: any, + badAuth: boolean = false +): Promise<{ + client: CaseClient; + services: { userActionService: jest.Mocked }; +}> => { + const log = loggingSystemMock.create().get('case'); + const request = {} as KibanaRequest; + + const caseServicePlugin = new CaseService(log); + const caseConfigureServicePlugin = new CaseConfigureService(log); + + const caseService = await caseServicePlugin.setup({ + authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), + }); + const caseConfigureService = await caseConfigureServicePlugin.setup(); + const userActionService = { + postUserActions: jest.fn(), + getUserActions: jest.fn(), + }; + + return { + client: createCaseClient({ + savedObjectsClient, + request, + caseService, + caseConfigureService, + userActionService, + }), + services: { userActionService }, + }; +}; diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts new file mode 100644 index 0000000000000..8db7d8a5747d7 --- /dev/null +++ b/x-pack/plugins/case/server/client/types.ts @@ -0,0 +1,46 @@ +/* + * 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 { KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { + CasePostRequest, + CasesPatchRequest, + CommentRequest, + CaseResponse, + CasesResponse, +} from '../../common/api'; +import { + CaseConfigureServiceSetup, + CaseServiceSetup, + CaseUserActionServiceSetup, +} from '../services'; + +export interface CaseClientCreate { + theCase: CasePostRequest; +} + +export interface CaseClientUpdate { + cases: CasesPatchRequest; +} + +export interface CaseClientAddComment { + caseId: string; + comment: CommentRequest; +} + +export interface CaseClientFactoryArguments { + savedObjectsClient: SavedObjectsClientContract; + request: KibanaRequest; + caseConfigureService: CaseConfigureServiceSetup; + caseService: CaseServiceSetup; + userActionService: CaseUserActionServiceSetup; +} + +export interface CaseClient { + create: (args: CaseClientCreate) => Promise; + update: (args: CaseClientUpdate) => Promise; + addComment: (args: CaseClientAddComment) => Promise; +} diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 9cf045da3e700..5398f8ed0ae83 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,10 +5,17 @@ */ import { first, map } from 'rxjs/operators'; -import { Logger, PluginInitializerContext } from 'kibana/server'; -import { CoreSetup } from 'src/core/server'; +import { + IContextProvider, + KibanaRequest, + Logger, + PluginInitializerContext, + RequestHandler, +} from 'kibana/server'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; +import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -18,7 +25,15 @@ import { caseCommentSavedObjectType, caseUserActionSavedObjectType, } from './saved_object_types'; -import { CaseConfigureService, CaseService, CaseUserActionService } from './services'; +import { + CaseConfigureService, + CaseConfigureServiceSetup, + CaseService, + CaseServiceSetup, + CaseUserActionService, + CaseUserActionServiceSetup, +} from './services'; +import { createCaseClient } from './client'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map((config) => config)); @@ -30,6 +45,9 @@ export interface PluginsSetup { export class CasePlugin { private readonly log: Logger; + private caseService?: CaseServiceSetup; + private caseConfigureService?: CaseConfigureServiceSetup; + private userActionService?: CaseUserActionServiceSetup; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); @@ -47,36 +65,83 @@ export class CasePlugin { core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseUserActionSavedObjectType); - const caseServicePlugin = new CaseService(this.log); - const caseConfigureServicePlugin = new CaseConfigureService(this.log); - const userActionServicePlugin = new CaseUserActionService(this.log); - this.log.debug( `Setting up Case Workflow with core contract [${Object.keys( core )}] and plugins [${Object.keys(plugins)}]` ); - const caseService = await caseServicePlugin.setup({ + this.caseService = await new CaseService(this.log).setup({ authentication: plugins.security != null ? plugins.security.authc : null, }); - const caseConfigureService = await caseConfigureServicePlugin.setup(); - const userActionService = await userActionServicePlugin.setup(); + this.caseConfigureService = await new CaseConfigureService(this.log).setup(); + this.userActionService = await new CaseUserActionService(this.log).setup(); + + core.http.registerRouteHandlerContext( + APP_ID, + this.createRouteHandlerContext({ + core, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, + }) + ); const router = core.http.createRouter(); initCaseApi({ - caseConfigureService, - caseService, - userActionService, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, router, }); } - public start() { + public async start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); + + const getCaseClientWithRequest = async (request: KibanaRequest) => { + return createCaseClient({ + savedObjectsClient: core.savedObjects.getScopedClient(request), + request, + caseService: this.caseService!, + caseConfigureService: this.caseConfigureService!, + userActionService: this.userActionService!, + }); + }; + + return { + getCaseClientWithRequest, + }; } public stop() { this.log.debug(`Stopping Case Workflow`); } + + private createRouteHandlerContext = ({ + core, + caseService, + caseConfigureService, + userActionService, + }: { + core: CoreSetup; + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + userActionService: CaseUserActionServiceSetup; + }): IContextProvider, typeof APP_ID> => { + return async (context, request) => { + const [{ savedObjects }] = await core.getStartServices(); + return { + getCaseClient: () => { + return createCaseClient({ + savedObjectsClient: savedObjects.getScopedClient(request), + caseService, + caseConfigureService, + userActionService, + request, + }); + }, + }; + }; + }; } diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index c2df91148a53a..8bbd419e6315b 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -39,7 +39,15 @@ export const createMockSavedObjectsRepository = ({ } const result = caseSavedObject.filter((s) => s.id === id); if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + return { + id, + type, + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [cases/not-exist] not found', + }, + }; } return result[0]; }), diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 265970b1abdec..e7ea381da9955 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import { ESCasesConfigureAttributes, CommentAttributes, @@ -325,3 +325,12 @@ export const mockCaseConfigure: Array> = version: 'WzYsMV0=', }, ]; + +export const mockCaseConfigureFind: Array> = [ + { + page: 1, + per_page: 5, + total: mockCaseConfigure.length, + saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index d947ffbaf181d..67890599fa417 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -4,13 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { createCaseClient } from '../../../client'; +import { CaseService, CaseConfigureService } from '../../../services'; import { getActions } from '../__mocks__/request_responses'; +import { authenticationMock } from '../__fixtures__'; -export const createRouteContext = (client: any) => { +export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + const log = loggingSystemMock.create().get('case'); + + const caseServicePlugin = new CaseService(log); + const caseConfigureServicePlugin = new CaseConfigureService(log); + + const caseService = await caseServicePlugin.setup({ + authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), + }); + const caseConfigureService = await caseConfigureServicePlugin.setup(); + const caseClient = createCaseClient({ + savedObjectsClient: client, + request: {} as KibanaRequest, + caseService, + caseConfigureService, + userActionService: { + postUserActions: jest.fn(), + getUserActions: jest.fn(), + }, + }); return ({ core: { @@ -19,5 +42,8 @@ export const createRouteContext = (client: any) => { }, }, actions: { getActionsClient: () => actionsMock }, + case: { + getCaseClient: () => caseClient, + }, } as unknown) as RequestHandlerContext; }; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index 67cb998409570..986ad3a54496f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -32,7 +32,7 @@ describe('DELETE comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -52,7 +52,7 @@ describe('DELETE comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index 24a03b217ab7c..23f64151a78d7 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -32,7 +32,7 @@ describe('GET comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -57,7 +57,7 @@ describe('GET comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseCommentSavedObject: mockCaseComments, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 04473e302e468..400e8ca404ca5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -35,7 +35,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -63,7 +63,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -87,7 +87,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 9006470f36f36..acc23815e3a39 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -26,6 +26,7 @@ describe('POST comment', () => { toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), })); }); + it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -38,7 +39,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -51,6 +52,7 @@ describe('POST comment', () => { 'mock-comment' ); }); + it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -63,7 +65,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -74,6 +76,7 @@ describe('POST comment', () => { expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); + it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -86,7 +89,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -97,6 +100,7 @@ describe('POST comment', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); + it(`Allow user to create comments without authentications`, async () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); @@ -111,11 +115,12 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, - }) + }), + true ); const response = await routeHandler(theContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 3c5b72eba5d13..08d442bccf2cb 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -5,24 +5,12 @@ */ import { schema } from '@kbn/config-schema'; -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { CaseResponseRt, CommentRequestRt, excess, throwErrors } from '../../../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; -import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; -import { escapeHatch, transformNewComment, wrapError, flattenCaseSavedObject } from '../../utils'; +import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentRequest } from '../../../../../common/api'; -export function initPostCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPostCommentApi({ router }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -34,101 +22,17 @@ export function initPostCommentApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const caseId = request.params.case_id; - const query = pipe( - excess(CommentRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myCase = await caseService.getCase({ - client, - caseId, - }); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const createdDate = new Date().toISOString(); - - const [newComment, updatedCase] = await Promise.all([ - caseService.postNewComment({ - client, - attributes: transformNewComment({ - createdDate, - ...query, - username, - full_name, - email, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: myCase.id, - }, - ], - }), - caseService.patchCase({ - client, - caseId, - updatedAttributes: { - updated_at: createdDate, - updated_by: { username, full_name, email }, - }, - version: myCase.version, - }), - ]); - - const totalCommentsFindByCases = await caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const [comments] = await Promise.all([ - caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: totalCommentsFindByCases.total, - }, - }), - userActionService.postUserActions({ - client, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: myCase.id, - commentId: newComment.id, - fields: ['comment'], - newValue: query.comment, - }), - ], - }), - ]); + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const comment = request.body as CommentRequest; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase.attributes }, - version: updatedCase.version ?? myCase.version, - references: myCase.references, - }, - comments: comments.saved_objects, - }) - ), + body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index 45ce19fca9d20..cc4f208758369 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -29,7 +29,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -49,7 +49,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], }) @@ -87,7 +87,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -105,7 +105,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index ee4dcc8e81b95..2eab4ac756361 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -29,7 +29,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -106,7 +106,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index 8fcb769225d44..261cd3e6b0884 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -39,7 +39,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -72,7 +72,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -110,7 +110,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -141,7 +141,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -163,7 +163,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -190,7 +190,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 27df19d8f823a..7ef3bdb4a700a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -37,7 +37,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -72,7 +72,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -112,7 +112,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -137,7 +137,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -162,7 +162,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -187,7 +187,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -212,7 +212,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -234,7 +234,7 @@ describe('POST configuration', () => { caseConfigureSavedObject: mockCaseConfigure, }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -253,7 +253,7 @@ describe('POST configuration', () => { caseConfigureSavedObject: [], }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -275,7 +275,7 @@ describe('POST configuration', () => { ], }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -291,7 +291,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -309,7 +309,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], }) @@ -334,7 +334,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -360,7 +360,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -385,7 +385,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -406,7 +406,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index e655339e05eb1..3970534140cd8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -32,7 +32,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -51,7 +51,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -70,7 +70,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCaseComments, @@ -89,7 +89,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCasesErrorTriggerData, diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index df27551d2c922..b2ba8b2fcb33a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -29,7 +29,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -46,7 +46,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -63,7 +63,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -80,7 +80,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 224da4464e1c2..01de9abac16af 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -39,7 +39,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -70,7 +70,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -94,7 +94,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -119,7 +119,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, }) @@ -142,7 +142,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -171,7 +171,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, @@ -201,7 +201,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index c0d19edcad91f..ea69ee77c5802 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -43,7 +43,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -93,7 +93,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -144,7 +144,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -170,7 +170,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -201,7 +201,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -232,7 +232,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -257,7 +257,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -283,7 +283,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 79e2e99731546..873671a909801 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -4,31 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { - CasesPatchRequestRt, - CasesResponseRt, - CasePatchRequest, - excess, - throwErrors, - ESCasePatchRequest, -} from '../../../../common/api'; -import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils'; +import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; -import { getCaseToUpdate, transformCaseConnectorToEsConnector } from './helpers'; -import { buildCaseUserActions } from '../../../services/user_actions/helpers'; import { CASES_URL } from '../../../../common/constants'; +import { CasesPatchRequest } from '../../../../common/api'; -export function initPatchCasesApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCasesApi({ router }: RouteDeps) { router.patch( { path: CASES_URL, @@ -37,126 +18,17 @@ export function initPatchCasesApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const query = pipe( - excess(CasesPatchRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myCases = await caseService.getCases({ - client, - caseIds: query.cases.map((q) => q.id), - }); - - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); - - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; - }); - if (nonExistingCases.length > 0) { - throw Boom.notFound( - `These cases ${nonExistingCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - if (conflictedCases.length > 0) { - throw Boom.conflict( - `These cases ${conflictedCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; - }); + const caseClient = context.case.getCaseClient(); + const cases = request.body as CasesPatchRequest; - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; + try { + return response.ok({ + body: await caseClient.update({ cases }), }); - - if (updateFilterCases.length > 0) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const updatedDt = new Date().toISOString(); - const updatedCases = await caseService.patchCases({ - client, - cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; - let closedInfo = {}; - if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') { - closedInfo = { - closed_at: updatedDt, - closed_by: { email, full_name, username }, - }; - } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') { - closedInfo = { - closed_at: null, - closed_by: null, - }; - } - return { - caseId, - updatedAttributes: { - ...updateCaseAttributes, - ...closedInfo, - updated_at: updatedDt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - - const returnUpdatedCase = myCases.saved_objects - .filter((myCase) => - updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) - ) - .map((myCase) => { - const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); - return flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - version: updatedCase?.version ?? myCase.version, - }, - }); - }); - - await userActionService.postUserActions({ - client, - actions: buildCaseUserActions({ - originalCases: myCases.saved_objects, - updatedCases: updatedCases.saved_objects, - actionDate: updatedDt, - actionBy: { email, full_name, username }, - }), - }); - - return response.ok({ - body: CasesResponseRt.encode(returnUpdatedCase), - }); - } - throw Boom.notAcceptable('All update fields are identical to current version.'); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index be1ed4166ab7b..1e1b19baa1c47 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -45,7 +45,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -80,7 +80,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -110,7 +110,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -132,7 +132,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -162,11 +162,12 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, - }) + }), + true ); const response = await routeHandler(theContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 5d8113b685741..663d502d548d5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -4,25 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; +import { wrapError, escapeHatch } from '../utils'; -import { flattenCaseSavedObject, transformNewCase, wrapError, escapeHatch } from '../utils'; - -import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../../../common/api'; -import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; -import { getConnectorFromConfiguration, transformCaseConnectorToEsConnector } from './helpers'; +import { CasePostRequest } from '../../../../common/api'; -export function initPostCaseApi({ - caseService, - caseConfigureService, - router, - userActionService, -}: RouteDeps) { +export function initPostCaseApi({ router }: RouteDeps) { router.post( { path: CASES_URL, @@ -31,53 +19,15 @@ export function initPostCaseApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const query = pipe( - excess(CasePostRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - - const newCase = await caseService.postNewCase({ - client, - attributes: transformNewCase({ - createdDate, - newCase: query, - username, - full_name, - email, - connector: transformCaseConnectorToEsConnector( - query.connector ?? caseConfigureConnector - ), - }), - }); - - await userActionService.postUserActions({ - client, - actions: [ - buildCaseUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector'], - newValue: JSON.stringify(query), - }), - ], - }); + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const caseClient = context.case.getCaseClient(); + const theCase = request.body as CasePostRequest; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ), + body: await caseClient.create({ theCase }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index c68b4b0c91735..eee59a974b37b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -44,7 +44,7 @@ describe('Push case', () => { body: caseExternalServiceRequestBody, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -66,7 +66,7 @@ describe('Push case', () => { body: caseExternalServiceRequestBody, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: [ @@ -97,7 +97,7 @@ describe('Push case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 2202bda2be087..90066bf29ae66 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -121,7 +121,7 @@ export const flattenCaseSavedObjects = ( export const flattenCaseSavedObject = ({ savedObject, comments = [], - totalComment = 0, + totalComment = comments.length, }: { savedObject: SavedObject; comments?: Array>; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 3db83331a0ab9..cab8cb499c3fa 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -96,7 +96,7 @@ interface PatchComments extends ClientArgs { interface GetUserArgs { request: KibanaRequest; - response: KibanaResponseFactory; + response?: KibanaResponseFactory; } interface CaseServiceDeps { diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts new file mode 100644 index 0000000000000..287f80a60ab07 --- /dev/null +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -0,0 +1,43 @@ +/* + * 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 { CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup } from '.'; + +export type CaseServiceMock = jest.Mocked; +export type CaseConfigureServiceMock = jest.Mocked; +export type CaseUserActionServiceMock = jest.Mocked; + +export const createCaseServiceMock = (): CaseServiceMock => ({ + deleteCase: jest.fn(), + deleteComment: jest.fn(), + findCases: jest.fn(), + getAllCaseComments: jest.fn(), + getCase: jest.fn(), + getCases: jest.fn(), + getComment: jest.fn(), + getTags: jest.fn(), + getReporters: jest.fn(), + getUser: jest.fn(), + postNewCase: jest.fn(), + postNewComment: jest.fn(), + patchCase: jest.fn(), + patchCases: jest.fn(), + patchComment: jest.fn(), + patchComments: jest.fn(), +}); + +export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({ + delete: jest.fn(), + get: jest.fn(), + find: jest.fn(), + patch: jest.fn(), + post: jest.fn(), +}); + +export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ + getUserActions: jest.fn(), + postUserActions: jest.fn(), +}); diff --git a/x-pack/plugins/case/server/types.ts b/x-pack/plugins/case/server/types.ts new file mode 100644 index 0000000000000..b95060ef30452 --- /dev/null +++ b/x-pack/plugins/case/server/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseClient } from './client'; + +export interface CaseRequestContext { + getCaseClient: () => CaseClient; +} + +declare module 'src/core/server' { + interface RequestHandlerContext { + case?: CaseRequestContext; + } +} From b1aa93f93195d6dd4df998a4f2718f53e7121c8e Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 27 Oct 2020 15:08:43 -0400 Subject: [PATCH 04/11] check for server enabled (#81818) --- .../ingest_manager_api_integration/apis/epm/data_stream.ts | 2 +- .../apis/epm/package_install_complete.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index d1d909f773a2b..3dbfdfb45008f 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -65,7 +65,7 @@ export default function (providerContext: FtrProviderContext) { }); }); afterEach(async () => { - if (!server) return; + if (!server.enabled) return; await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts index 6fd4b64f0ee5e..2e7ab199a7fbc 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -16,6 +16,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const pkgName = 'multiple_versions'; const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; @@ -23,6 +25,7 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); describe('package install', async () => { before(async () => { + if (!server.enabled) return; await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -84,6 +87,7 @@ export default function (providerContext: FtrProviderContext) { expect(packageAfterSetup.attributes.install_status).equal('installing'); }); after(async () => { + if (!server.enabled) return; await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -92,6 +96,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('package update', async () => { before(async () => { + if (!server.enabled) return; await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -164,6 +169,7 @@ export default function (providerContext: FtrProviderContext) { expect(packageAfterSetup.attributes.version).equal(pkgVersion); }); after(async () => { + if (!server.enabled) return; await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) .set('kbn-xsrf', 'xxxx') From 5996bc3e3b871fc9f60bb945212b52faec54661f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 27 Oct 2020 12:58:12 -0700 Subject: [PATCH 05/11] skip flaky suite (#81844) --- x-pack/test/api_integration/apis/maps/proxy_api.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/maps/proxy_api.js b/x-pack/test/api_integration/apis/maps/proxy_api.js index f85de9dc1670a..45ce84cabcac9 100644 --- a/x-pack/test/api_integration/apis/maps/proxy_api.js +++ b/x-pack/test/api_integration/apis/maps/proxy_api.js @@ -9,7 +9,8 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - describe('EMS proxy', () => { + // Failing: See https://github.com/elastic/kibana/issues/81844 + describe.skip('EMS proxy', () => { it('should correctly rewrite url and format', async () => { const resp = await supertest .get(`/api/maps/ems/files/v7.10/manifest`) From d1827dc461a4c368240f6b24b79c8f45b0c142ab Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 27 Oct 2020 15:18:30 -0500 Subject: [PATCH 06/11] Add tsconfig for url_forwarding (#81177) Also add missing pieces to kibana_react, as a follow-up to #80992. References #80508 References #81003 --- src/core/public/apm_system.test.ts | 1 + src/core/public/chrome/chrome_service.mock.ts | 1 + .../notifications_service.mock.ts | 1 + .../public/overlays/overlay_service.mock.ts | 1 + src/core/server/core_context.mock.ts | 1 + src/core/server/elasticsearch/client/mocks.ts | 1 + src/core/server/mocks.ts | 1 + src/core/typings.ts | 4 +-- .../public/angular/kbn_top_nav.d.ts | 31 ++++++++++++++++++ .../public/angular/promises.d.ts | 20 ++++++++++++ .../public/angular/watch_multi.d.ts | 20 ++++++++++++ .../public/utils/kbn_accessible_click.d.ts | 27 ++++++++++++++++ .../kibana_legacy/public/utils/private.d.ts | 4 +++ .../utils/register_listen_event_listener.d.ts | 20 ++++++++++++ src/plugins/url_forwarding/tsconfig.json | 15 +++++++++ tsconfig.json | 10 +++--- tsconfig.refs.json | 12 ++++--- x-pack/tsconfig.json | 32 ++++++++----------- 18 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts create mode 100644 src/plugins/kibana_legacy/public/angular/promises.d.ts create mode 100644 src/plugins/kibana_legacy/public/angular/watch_multi.d.ts create mode 100644 src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts create mode 100644 src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts create mode 100644 src/plugins/url_forwarding/tsconfig.json diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts index f88cdd899ef81..8467076c31542 100644 --- a/src/core/public/apm_system.test.ts +++ b/src/core/public/apm_system.test.ts @@ -19,6 +19,7 @@ jest.mock('@elastic/apm-rum'); import { init, apm } from '@elastic/apm-rum'; +import { DeeplyMockedKeys } from '../typings'; import { ApmSystem } from './apm_system'; const initMock = init as jest.Mocked; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 9cd96763d2e79..683d153288185 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import { DeeplyMockedKeys } from '../../typings'; import { ChromeBadge, ChromeBrand, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; const createStartContractMock = () => { diff --git a/src/core/public/notifications/notifications_service.mock.ts b/src/core/public/notifications/notifications_service.mock.ts index 990ab479d35c3..521ce52c90d0a 100644 --- a/src/core/public/notifications/notifications_service.mock.ts +++ b/src/core/public/notifications/notifications_service.mock.ts @@ -17,6 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; +import { MockedKeys } from '../../typings'; import { NotificationsService, NotificationsSetup, diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 66ba36b20b45c..72a51b0b14187 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -17,6 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; +import { DeeplyMockedKeys } from '../../typings'; import { OverlayService, OverlayStart } from './overlay_service'; import { overlayBannersServiceMock } from './banners/banners_service.mock'; import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index bbf04783278f7..8bbba586d46f5 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -18,6 +18,7 @@ */ import { REPO_ROOT } from '@kbn/dev-utils'; +import { DeeplyMockedKeys } from '../typings'; import { CoreContext } from './core_context'; import { Env, IConfigService } from './config'; import { configServiceMock, getEnvOptions } from './config/mocks'; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index fb2826c787718..260efa40ac43c 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -18,6 +18,7 @@ */ import { Client, ApiResponse } from '@elastic/elasticsearch'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { DeeplyMockedKeys } from '../../../typings'; import { ElasticsearchClient } from './types'; import { ICustomClusterClient } from './cluster_client'; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index e47d06409894e..1c0584837dc3a 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -19,6 +19,7 @@ import { of } from 'rxjs'; import { duration } from 'moment'; import { ByteSizeValue } from '@kbn/config-schema'; +import { MockedKeys } from '../typings'; import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; import { loggingSystemMock } from './logging/logging_system.mock'; import { loggingServiceMock } from './logging/logging_service.mock'; diff --git a/src/core/typings.ts b/src/core/typings.ts index f271d0b03e0d3..fe182ddfb131b 100644 --- a/src/core/typings.ts +++ b/src/core/typings.ts @@ -17,11 +17,11 @@ * under the License. */ -type DeeplyMockedKeys = { +export type DeeplyMockedKeys = { [P in keyof T]: T[P] extends (...args: any[]) => any ? jest.MockInstance, Parameters> : DeeplyMockedKeys; } & T; -type MockedKeys = { [P in keyof T]: jest.Mocked }; +export type MockedKeys = { [P in keyof T]: jest.Mocked }; diff --git a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts new file mode 100644 index 0000000000000..3d1dcdbef3f4b --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; + +export const createTopNavDirective: Injectable>; +export const createTopNavHelper: ( + options: unknown +) => Injectable>; +export function loadKbnTopNavDirectives(navUi: unknown): void; diff --git a/src/plugins/kibana_legacy/public/angular/promises.d.ts b/src/plugins/kibana_legacy/public/angular/promises.d.ts new file mode 100644 index 0000000000000..1a2ce66834d7b --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular/promises.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function PromiseServiceCreator($q: unknown, $timeout: unknown): (fn: unknown) => unknown; diff --git a/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts b/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts new file mode 100644 index 0000000000000..7c73abf2f9aa2 --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function watchMultiDecorator($provide: unknown): void; diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts new file mode 100644 index 0000000000000..e4ef43fe8d443 --- /dev/null +++ b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; + +export const KbnAccessibleClickProvider: Injectable>; diff --git a/src/plugins/kibana_legacy/public/utils/private.d.ts b/src/plugins/kibana_legacy/public/utils/private.d.ts index 3efc9cd5308f7..00b0220316ead 100644 --- a/src/plugins/kibana_legacy/public/utils/private.d.ts +++ b/src/plugins/kibana_legacy/public/utils/private.d.ts @@ -17,4 +17,8 @@ * under the License. */ +import { IServiceProvider } from 'angular'; + export type IPrivate = (provider: (...injectable: any[]) => T) => T; + +export function PrivateProvider(): IServiceProvider; diff --git a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts new file mode 100644 index 0000000000000..eff9b4b871f56 --- /dev/null +++ b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function registerListenEventListener($rootScope: unknown): void; diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json new file mode 100644 index 0000000000000..8e867a6bad14f --- /dev/null +++ b/src/plugins/url_forwarding/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 73646291e3d08..49a45f370701d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,13 +9,15 @@ "src/test_utils/**/*", "src/core/**/*", "src/plugins/kibana_legacy/**/*", + "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/kibana_react/**/*", - "src/plugins/usage_collection/**/*", - "src/plugins/telemetry_collection_manager/**/*", + "src/plugins/kibana_utils/**/*", + "src/plugins/newsfeed/**/*", "src/plugins/telemetry/**/*", - "src/plugins/kibana_usage_collection/**/*", - "src/plugins/newsfeed/**/*" + "src/plugins/telemetry_collection_manager/**/*", + "src/plugins/url_forwarding/**/*", + "src/plugins/usage_collection/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find diff --git a/tsconfig.refs.json b/tsconfig.refs.json index bb1bdc08cafd6..6247761812581 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -1,14 +1,16 @@ { "include": [], "references": [ - { "path": "./src/test_utils/tsconfig.json" }, { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/test_utils/tsconfig.json" }, + { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/url_forwarding/tsconfig.json" }, + { "path": "./src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 382d4c073d41e..057441304f093 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,20 +1,14 @@ { "extends": "../tsconfig.base.json", - "include": [ - "mocks.ts", - "typings/**/*", - "plugins/**/*", - "test_utils/**/*", - "tasks/**/*" - ], + "include": ["mocks.ts", "typings/**/*", "plugins/**/*", "test_utils/**/*", "tasks/**/*"], "exclude": [ - "test/**/*", - "plugins/security_solution/cypress/**/*", "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", - "plugins/licensing/**/*", "plugins/global_search/**/*", - "plugins/telemetry_collection_xpack/**/*" + "plugins/licensing/**/*", + "plugins/security_solution/cypress/**/*", + "plugins/telemetry_collection_xpack/**/*", + "test/**/*" ], "compilerOptions": { "paths": { @@ -28,15 +22,17 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, - { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, + { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" } ] } From 07930895cff6a0267bf37453bc109341a903d9fc Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 27 Oct 2020 14:22:33 -0700 Subject: [PATCH 07/11] skip flaky suite (#81853) --- .../test_suites/task_manager/task_management.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index 1fd313c1ac437..348ff35b2968f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -57,7 +57,8 @@ export default function ({ getService }: FtrProviderContext) { const testHistoryIndex = '.kibana_task_manager_test_result'; const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); - describe('scheduling and running tasks', () => { + // Failing: See https://github.com/elastic/kibana/issues/81853 + describe.skip('scheduling and running tasks', () => { beforeEach( async () => await supertest.delete('/api/sample_tasks').set('kbn-xsrf', 'xxx').expect(200) ); From 2788dca390fd8cef4119f4e8038f4fda7818f608 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 27 Oct 2020 20:28:48 -0400 Subject: [PATCH 08/11] [Security Solutions][Detections] - Fix bug, last response not showing for disabled rules (#81783) ## Summary **Bug fix addressed in this PR:** - fixes https://github.com/elastic/kibana/issues/63203 - in the backend, we were only fetching status data for enabled rules, changed to fetch status data regardles of whether rule is enabled or disabled --- .../cypress/screens/alerts_detection_rules.ts | 4 +- .../__snapshots__/index.test.tsx.snap | 20 --- .../rules/rule_switch/index.test.tsx | 167 +++++++++++++++++- .../components/rules/rule_switch/index.tsx | 29 +-- .../routes/rules/patch_rules_bulk_route.ts | 7 +- .../routes/rules/update_rules_bulk_route.ts | 8 +- .../routes/rules/validate.test.ts | 68 ++++++- .../detection_engine/routes/rules/validate.ts | 3 +- 8 files changed, 243 insertions(+), 63 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index f68ad88f578c7..0d0ea8460edf1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -47,9 +47,9 @@ export const RULE_CHECKBOX = '.euiTableRow .euiCheckbox__input'; export const RULE_NAME = '[data-test-subj="ruleName"]'; -export const RULE_SWITCH = '[data-test-subj="rule-switch"]'; +export const RULE_SWITCH = '[data-test-subj="ruleSwitch"]'; -export const RULE_SWITCH_LOADER = '[data-test-subj="rule-switch-loader"]'; +export const RULE_SWITCH_LOADER = '[data-test-subj="ruleSwitchLoader"]'; export const RULES_TABLE = '[data-test-subj="rules-table"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 604f86866d565..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RuleSwitch renders correctly against snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 104eff34c91b3..910a28927fd93 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -4,16 +4,173 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { waitFor } from '@testing-library/react'; +import { enableRules } from '../../../containers/detection_engine/rules'; +import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; import { RuleSwitchComponent } from './index'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; + +jest.mock('../../../../common/components/toasters'); +jest.mock('../../../containers/detection_engine/rules'); +jest.mock('../../../pages/detection_engine/rules/all/actions'); describe('RuleSwitch', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - + beforeEach(() => { + (useStateToaster as jest.Mock).mockImplementation(() => [[], jest.fn()]); + (enableRules as jest.Mock).mockResolvedValue([getRulesSchemaMock()]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').exists()).toBeFalsy(); + }); + + test('it renders switch disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); + }); + + test('it renders switch enabled if "enabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); + }); + + test('it renders switch disabled if "enabled" is false', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); + }); + + test('it renders an off switch enabled on click', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeTruthy(); + }); + }); + + test('it renders an on switch off on click', async () => { + const rule: RulesSchema = { ...getRulesSchemaMock(), enabled: false }; + + (enableRules as jest.Mock).mockResolvedValue([rule]); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeFalsy(); + }); + }); + + test('it dispatches error toaster if "enableRules" call rejects', async () => { + const mockError = new Error('uh oh'); + (enableRules as jest.Mock).mockRejectedValue(mockError); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + ); - expect(wrapper).toMatchSnapshot(); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(displayErrorToast).toHaveBeenCalledTimes(1); + }); + }); + + test('it dispatches error toaster if "enableRules" call resolves with some errors', async () => { + (enableRules as jest.Mock).mockResolvedValue([ + getRulesSchemaMock(), + { error: { status_code: 400, message: 'error' } }, + { error: { status_code: 400, message: 'error' } }, + ]); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(displayErrorToast).toHaveBeenCalledTimes(1); + }); + }); + + test('it invokes "enableRulesAction" if dispatch is passed through', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(enableRulesAction).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index 73d66bf024a62..1a9bcca7eb601 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import * as i18n from '../../../pages/detection_engine/rules/translations'; import { enableRules } from '../../../containers/detection_engine/rules'; @@ -63,8 +63,11 @@ export const RuleSwitchComponent = ({ if (dispatch != null) { await enableRulesAction([id], event.target.checked!, dispatch, dispatchToaster); } else { + const enabling = event.target.checked!; + const title = enabling + ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) + : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); try { - const enabling = event.target.checked!; const response = await enableRules({ ids: [id], enabled: enabling, @@ -73,9 +76,7 @@ export const RuleSwitchComponent = ({ if (errors.length > 0) { setMyIsLoading(false); - const title = enabling - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); + displayErrorToast( title, errors.map((e) => e.error.message), @@ -88,8 +89,9 @@ export const RuleSwitchComponent = ({ onChange(rule.enabled); } } - } catch { + } catch (err) { setMyIsLoading(false); + displayErrorToast(title, err.message, dispatchToaster); } } setMyIsLoading(false); @@ -105,21 +107,22 @@ export const RuleSwitchComponent = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled]); - useEffect(() => { + const showLoader = useMemo((): boolean => { if (myIsLoading !== isLoading) { - setMyIsLoading(isLoading ?? false); + return isLoading ?? false; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); + + return myIsLoading; + }, [myIsLoading, isLoading]); return ( - {myIsLoading ? ( - + {showLoader ? ( + ) : ( search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError( - rule.id, - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 8828bbe6c9826..ddc2ade9b5ac9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -50,7 +50,6 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( @@ -192,12 +191,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError( - rule.id, - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 06ec22b2f61b4..6bdbfedf625dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -9,13 +9,13 @@ import { transformValidateFindAlerts, transformValidateBulkError, } from './validate'; -import { getResult } from '../__mocks__/request_responses'; import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; +import { getResult, getFindResultStatus } from '../__mocks__/request_responses'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; -export const ruleOutput: RulesSchema = { +export const ruleOutput = (): RulesSchema => ({ actions: [], author: ['Elastic'], created_at: '2019-12-13T16:40:33.400Z', @@ -80,14 +80,14 @@ export const ruleOutput: RulesSchema = { note: '# Investigative notes', timeline_title: 'some-timeline-title', timeline_id: 'some-timeline-id', -}; +}); describe('validate', () => { describe('transformValidate', () => { test('it should do a validation correctly of a partial alert', () => { const ruleAlert = getResult(); const [validated, errors] = transformValidate(ruleAlert); - expect(validated).toEqual(ruleOutput); + expect(validated).toEqual(ruleOutput()); expect(errors).toEqual(null); }); @@ -103,14 +103,35 @@ describe('validate', () => { describe('transformValidateFindAlerts', () => { test('it should do a validation correctly of a find alert', () => { - const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + const findResult: FindResult = { + data: [getResult()], + page: 1, + perPage: 0, + total: 0, + }; const [validated, errors] = transformValidateFindAlerts(findResult, []); - expect(validated).toEqual({ data: [ruleOutput], page: 1, perPage: 0, total: 0 }); + const expected: { + page: number; + perPage: number; + total: number; + data: Array>; + } | null = { + data: [ruleOutput()], + page: 1, + perPage: 0, + total: 0, + }; + expect(validated).toEqual(expected); expect(errors).toEqual(null); }); test('it should do an in-validation correctly of a partial alert', () => { - const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + const findResult: FindResult = { + data: [getResult()], + page: 1, + perPage: 0, + total: 0, + }; // @ts-expect-error delete findResult.page; const [validated, errors] = transformValidateFindAlerts(findResult, []); @@ -123,7 +144,7 @@ describe('validate', () => { test('it should do a validation correctly of a rule id', () => { const ruleAlert = getResult(); const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); - expect(validatedOrError).toEqual(ruleOutput); + expect(validatedOrError).toEqual(ruleOutput()); }); test('it should do an in-validation correctly of a rule id', () => { @@ -140,5 +161,34 @@ describe('validate', () => { }; expect(validatedOrError).toEqual(expected); }); + + test('it should do a validation correctly of a rule id with ruleStatus passed in', () => { + const ruleStatus = getFindResultStatus(); + const ruleAlert = getResult(); + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatus); + const expected: RulesSchema = { + ...ruleOutput(), + status: 'succeeded', + status_date: '2020-02-18T15:26:49.783Z', + last_success_at: '2020-02-18T15:26:49.783Z', + last_success_message: 'succeeded', + }; + expect(validatedOrError).toEqual(expected); + }); + + test('it should return error object if "alert" is not expected alert type', () => { + const ruleAlert = getResult(); + // @ts-expect-error + delete ruleAlert.alertTypeId; + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); + const expected: BulkError = { + error: { + message: 'Internal error transforming', + status_code: 500, + }, + rule_id: 'rule-1', + }; + expect(validatedOrError).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 983382b28ab38..27100eaebea15 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -22,6 +22,7 @@ import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, isRuleStatusFindType, + IRuleStatusSOAttributes, } from '../../rules/types'; import { createBulkErrorObject, BulkError } from '../utils'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; @@ -74,7 +75,7 @@ export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, ruleActions?: RuleActions | null, - ruleStatus?: unknown + ruleStatus?: SavedObjectsFindResponse ): RulesSchema | BulkError => { if (isAlertType(alert)) { if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { From d869f558c3bd2736702a393b8e7f6b487ce5d0a6 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 27 Oct 2020 20:32:22 -0400 Subject: [PATCH 09/11] [Maps] Fix EMS test (#81856) --- x-pack/test/api_integration/apis/maps/proxy_api.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/maps/proxy_api.js b/x-pack/test/api_integration/apis/maps/proxy_api.js index 45ce84cabcac9..5dc6b372ced70 100644 --- a/x-pack/test/api_integration/apis/maps/proxy_api.js +++ b/x-pack/test/api_integration/apis/maps/proxy_api.js @@ -9,8 +9,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/81844 - describe.skip('EMS proxy', () => { + describe('EMS proxy', () => { it('should correctly rewrite url and format', async () => { const resp = await supertest .get(`/api/maps/ems/files/v7.10/manifest`) @@ -22,7 +21,7 @@ export default function ({ getService }) { //Check world-layer const worldLayer = resp.body.layers.find((layer) => layer.layer_id === 'world_countries'); expect(worldLayer.formats.length).to.be.greaterThan(0); - expect(worldLayer.formats[0].type).to.be('geojson'); + expect(worldLayer.formats[0].type).to.be('topojson'); expect(worldLayer.formats[0].url).to.be('file?id=world_countries'); }); }); From 44b75685f5ba84a5e9a07f64dd70fc0e1bfeec02 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 28 Oct 2020 10:52:55 +0200 Subject: [PATCH 10/11] [Security Solution][Case] Fix connector's labeling (#81824) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/cases/components/create/translations.ts | 2 +- .../security_solution/public/cases/pages/translations.ts | 2 +- x-pack/plugins/security_solution/public/cases/translations.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts index 222f8913b3fbd..38916dbddc7d7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts @@ -18,6 +18,6 @@ export const STEP_ONE_TITLE = i18n.translate( export const STEP_TWO_TITLE = i18n.translate( 'xpack.securitySolution.components.create.stepTwoTitle', { - defaultMessage: 'External incident management system fields', + defaultMessage: 'External Connector Fields', } ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts index 5b595c5892ef2..8ba4c4faf1876 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts @@ -224,7 +224,7 @@ export const GO_TO_DOCUMENTATION = i18n.translate( ); export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { - defaultMessage: 'External incident management system', + defaultMessage: 'External Incident Management System', }); export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index 003439422442b..a0b5f71db7df0 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -224,7 +224,7 @@ export const GO_TO_DOCUMENTATION = i18n.translate( ); export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { - defaultMessage: 'External incident management system', + defaultMessage: 'External Incident Management System', }); export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { From fb28424b39c8dd9d17195996491a773a4dcf7df6 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 28 Oct 2020 10:50:01 +0100 Subject: [PATCH 11/11] skips overview tests (#81877) --- .../security_solution/cypress/integration/overview.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 7ccd588e16a89..542cf4ad8178f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -12,7 +12,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; import { esArchiverUnload, esArchiverLoad } from '../tasks/es_archiver'; -describe('Overview Page', () => { +describe.skip('Overview Page', () => { before(() => { cy.stubSearchStrategyApi('overviewHostQuery', 'overview_search_strategy'); cy.stubSearchStrategyApi('overviewNetworkQuery', 'overview_search_strategy'); @@ -35,7 +35,7 @@ describe('Overview Page', () => { }); }); - describe('with no data', () => { + describe.skip('with no data', () => { before(() => { esArchiverUnload('auditbeat'); loginAndWaitForPage(OVERVIEW_URL);