From c71dbfaf9855a5a80f3d4f709be296a24fc51cbe Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 25 Aug 2021 16:43:22 -0400 Subject: [PATCH 1/7] Working export but not import --- x-pack/plugins/cases/server/plugin.ts | 4 +- .../cases/server/saved_object_types/cases.ts | 27 ++++- .../server/saved_object_types/comments.ts | 8 +- .../import_export/export.ts | 114 ++++++++++++++++++ .../cases/server/saved_object_types/index.ts | 2 +- 5 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/cases/server/saved_object_types/import_export/export.ts diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 49220fc7160340..5da591a1888f67 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -21,7 +21,7 @@ import { createCaseCommentSavedObjectType, caseConfigureSavedObjectType, caseConnectorMappingsSavedObjectType, - caseSavedObjectType, + createCaseSavedObjectType, caseUserActionSavedObjectType, subCaseSavedObjectType, } from './saved_object_types'; @@ -96,7 +96,7 @@ export class CasePlugin { ); core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); - core.savedObjects.registerType(caseSavedObjectType); + core.savedObjects.registerType(createCaseSavedObjectType(core, this.log)); core.savedObjects.registerType(caseUserActionSavedObjectType); this.log.debug( diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 199017c36fa3e8..a362d77c066263 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -5,11 +5,22 @@ * 2.0. */ -import { SavedObjectsType } from 'src/core/server'; +import { + CoreSetup, + Logger, + SavedObject, + SavedObjectsExportTransformContext, + SavedObjectsType, +} from 'src/core/server'; import { CASE_SAVED_OBJECT } from '../../common'; +import { ESCaseAttributes } from '../services/cases/types'; +import { handleExport } from './import_export/export'; import { caseMigrations } from './migrations'; -export const caseSavedObjectType: SavedObjectsType = { +export const createCaseSavedObjectType = ( + coreSetup: CoreSetup, + logger: Logger +): SavedObjectsType => ({ name: CASE_SAVED_OBJECT, hidden: true, namespaceType: 'single', @@ -144,4 +155,14 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, migrations: caseMigrations, -}; + management: { + importableAndExportable: true, + defaultSearchField: 'title', + icon: 'folderExclamation', + getTitle: (savedObject: SavedObject) => savedObject.attributes.title, + onExport: async ( + context: SavedObjectsExportTransformContext, + objects: Array> + ) => handleExport({ context, objects, coreSetup, logger }), + }, +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 0384a65dcb3891..a0b35f705d4a66 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsType } from 'src/core/server'; +import { SavedObject, SavedObjectsType } from 'src/core/server'; import { CASE_COMMENT_SAVED_OBJECT } from '../../common'; import { createCommentsMigrations, CreateCommentsMigrationsDeps } from './migrations'; @@ -109,5 +109,9 @@ export const createCaseCommentSavedObjectType = ({ }, }, }, - migrations: () => createCommentsMigrations(migrationDeps), + migrations: createCommentsMigrations(migrationDeps), + management: { + importableAndExportable: true, + isExportable: (_: SavedObject) => false, + }, }); diff --git a/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts new file mode 100644 index 00000000000000..d089079314443b --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CoreSetup, + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsExportTransformContext, +} from 'kibana/server'; +import { + CaseUserActionAttributes, + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, + CommentAttributes, + MAX_DOCS_PER_PAGE, + SAVED_OBJECT_TYPES, +} from '../../../common'; +import { createCaseError, defaultSortField } from '../../common'; +import { ESCaseAttributes } from '../../services/cases/types'; + +export async function handleExport({ + context, + objects, + coreSetup, + logger, +}: { + context: SavedObjectsExportTransformContext; + objects: Array>; + coreSetup: CoreSetup; + logger: Logger; +}): Promise>> { + try { + if (objects.length <= 0) { + return []; + } + + const [{ savedObjects }] = await coreSetup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(context.request, { + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); + + const caseIds = objects.map((caseObject) => caseObject.id); + const attachmentsAndUserActionsForCases = await getAttachmentsAndUserActionsForCases( + savedObjectsClient, + caseIds + ); + + return [...objects, ...attachmentsAndUserActionsForCases.flat()]; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve associated objects for exporting of cases: ${error}`, + error, + logger, + }); + } +} + +async function getAttachmentsAndUserActionsForCases( + savedObjectsClient: SavedObjectsClientContract, + caseIds: string[] +): Promise>> { + const [attachments, userActions] = await Promise.all([ + getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField: defaultSortField, + type: CASE_COMMENT_SAVED_OBJECT, + }), + getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField: 'action_at', + type: CASE_USER_ACTION_SAVED_OBJECT, + }), + ]); + + return [...attachments, ...userActions]; +} + +async function getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField, + type, +}: { + savedObjectsClient: SavedObjectsClientContract; + caseIds: string[]; + sortField: string; + type: string; +}): Promise>> { + const references = caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })); + + const finder = savedObjectsClient.createPointInTimeFinder({ + type, + hasReferenceOperator: 'OR', + hasReference: references, + perPage: MAX_DOCS_PER_PAGE, + sortField, + sortOrder: 'asc', + }); + + let result: Array> = []; + for await (const findResults of finder.find()) { + result = result.concat(findResults.saved_objects); + } + + return result; +} diff --git a/x-pack/plugins/cases/server/saved_object_types/index.ts b/x-pack/plugins/cases/server/saved_object_types/index.ts index 2c39a10f61da70..f6b87d1d480c1b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { caseSavedObjectType } from './cases'; +export { createCaseSavedObjectType } from './cases'; export { subCaseSavedObjectType } from './sub_case'; export { caseConfigureSavedObjectType } from './configure'; export { createCaseCommentSavedObjectType } from './comments'; From 095311103ef4e5102b402eed9bc4f0324ed57a15 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 25 Aug 2021 16:56:30 -0400 Subject: [PATCH 2/7] Adding user action import flag --- .../plugins/cases/server/saved_object_types/user_actions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 16bb7ac09a6eff..b3a3a2e99528a6 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsType } from 'src/core/server'; +import { SavedObject, SavedObjectsType } from 'src/core/server'; import { CASE_USER_ACTION_SAVED_OBJECT } from '../../common'; import { userActionsMigrations } from './migrations'; @@ -49,4 +49,8 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { }, }, migrations: userActionsMigrations, + management: { + importableAndExportable: true, + isExportable: (_: SavedObject) => false, + }, }; From 756283f103b540aa6f872fdbf337dbd9f24370c4 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 26 Aug 2021 17:09:50 -0400 Subject: [PATCH 3/7] Adding a few integration tests --- .../server/saved_object_types/comments.ts | 3 +- .../server/saved_object_types/user_actions.ts | 3 +- ...ingle_case_user_actions_one_comment.ndjson | 5 + ..._case_with_connector_update_to_none.ndjson | 6 + .../tests/common/cases/export.ts | 160 ++++++++++++++++++ .../security_and_spaces/tests/common/index.ts | 1 + 6 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson create mode 100644 x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index a0b35f705d4a66..2eaffff658ced9 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectsType } from 'src/core/server'; +import { SavedObjectsType } from 'src/core/server'; import { CASE_COMMENT_SAVED_OBJECT } from '../../common'; import { createCommentsMigrations, CreateCommentsMigrationsDeps } from './migrations'; @@ -112,6 +112,5 @@ export const createCaseCommentSavedObjectType = ({ migrations: createCommentsMigrations(migrationDeps), management: { importableAndExportable: true, - isExportable: (_: SavedObject) => false, }, }); diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index b3a3a2e99528a6..883105982bcb37 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectsType } from 'src/core/server'; +import { SavedObjectsType } from 'src/core/server'; import { CASE_USER_ACTION_SAVED_OBJECT } from '../../common'; import { userActionsMigrations } from './migrations'; @@ -51,6 +51,5 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { migrations: userActionsMigrations, management: { importableAndExportable: true, - isExportable: (_: SavedObject) => false, }, }; diff --git a/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson b/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson new file mode 100644 index 00000000000000..79442d5feeb77e --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson @@ -0,0 +1,5 @@ +{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T19:48:01.292Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"a description","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual","updated_at":"2021-08-26T19:48:30.151Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases":"7.15.0"},"references":[],"type":"cases","updated_at":"2021-08-26T19:48:30.162Z","version":"WzM0NDEsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T19:48:01.292Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case to export\",\"tags\":[\"some tags\"],\"description\":\"a description\",\"connector\":{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"8cb85070-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007281292,7288],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:13.687Z","version":"WzIzODIsMV0="} +{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-08-26T19:48:30.151Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.0.0","id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-comments":"7.15.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007310151,9470],"type":"cases-comments","updated_at":"2021-08-26T19:48:30.161Z","version":"WzM0NDIsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T19:48:30.151Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["comment"],"new_value":"{\"comment\":\"A comment for my case\",\"type\":\"user\",\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"9710c840-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"},{"id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1630007310151,9542],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:31.044Z","version":"WzM1MTIsMV0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]} diff --git a/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson b/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson new file mode 100644 index 00000000000000..4f4476df376cc6 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson @@ -0,0 +1,6 @@ +{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://cases-testing.atlassian.net","projectKey":"TPN"},"isMissingSecrets":true,"name":"A jira connector"},"coreMigrationVersion":"8.0.0","id":"1cd34740-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"action":"7.14.0"},"references":[],"type":"action","updated_at":"2021-08-26T20:35:12.447Z","version":"WzM1ODQsMV0="} +{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T20:35:42.131Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"super description","external_service":{"connector_name":"A jira connector","external_id":"10125","external_title":"TPN-118","external_url":"https://cases-testing.atlassian.net/browse/TPN-118","pushed_at":"2021-08-26T20:35:44.302Z","pushed_by":{"email":null,"full_name":null,"username":"elastic"}},"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["other tags"],"title":"A case with a connector","type":"individual","updated_at":"2021-08-26T20:36:35.536Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases":"7.15.0"},"references":[{"id":"1cd34740-06ad-11ec-babc-0b08808e8e01","name":"pushConnectorId","type":"action"}],"type":"cases","updated_at":"2021-08-26T20:36:35.537Z","version":"WzM1OTIsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T20:35:42.131Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case with a connector\",\"tags\":[\"other tags\"],\"description\":\"super description\",\"connector\":{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2e9db8c0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010142131,4024],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:42.284Z","version":"WzM1ODksMV0="} +{"attributes":{"action":"push-to-service","action_at":"2021-08-26T20:35:44.302Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["pushed"],"new_value":"{\"pushed_at\":\"2021-08-26T20:35:44.302Z\",\"pushed_by\":{\"username\":\"elastic\",\"full_name\":null,\"email\":null},\"connector_id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"connector_name\":\"A jira connector\",\"external_id\":\"10125\",\"external_title\":\"TPN-118\",\"external_url\":\"https://cases-testing.atlassian.net/browse/TPN-118\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2fd1cbf0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010144302,4029],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:44.303Z","version":"WzM1OTAsMV0="} +{"attributes":{"action":"update","action_at":"2021-08-26T20:36:35.536Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["connector"],"new_value":"{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null}","old_value":"{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}}","owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"4ee9b250-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010195536,4033],"type":"cases-user-actions","updated_at":"2021-08-26T20:36:36.469Z","version":"WzM1OTMsMV0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":5,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts new file mode 100644 index 00000000000000..986204ad0a4c64 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { join } from 'path'; +import { + deleteAllCaseItems, + createCase, + createComment, + findCases, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AttributesTypeUser, + CommentsResponse, + CASES_URL, + CaseType, +} from '../../../../../../plugins/cases/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('export cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('exports a case with its associated user actions and comments', async () => { + const caseRequest = getPostCaseRequest(); + const postedCase = await createCase(supertest, caseRequest); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const { text } = await supertest + .post(`/api/saved_objects/_export`) + .send({ + type: ['cases'], + excludeExportDetails: true, + includeReferencesDeep: true, + }) + .set('kbn-xsrf', 'true'); + + const objects = ndjsonToObject(text); + + expect(objects).to.have.length(4); + + // should be the case + expect(objects[0].attributes.title).to.eql(caseRequest.title); + expect(objects[0].attributes.description).to.eql(caseRequest.description); + expect(objects[0].attributes.connector.type).to.eql(caseRequest.connector.type); + expect(objects[0].attributes.connector.name).to.eql(caseRequest.connector.name); + expect(objects[0].attributes.connector.fields).to.eql([]); + expect(objects[0].attributes.settings).to.eql(caseRequest.settings); + + // should be two user actions + expect(objects[1].attributes.action).to.eql('create'); + + const parsedCaseNewValue = JSON.parse(objects[1].attributes.new_value); + const { + connector: { id: ignoreParsedId, ...restParsedConnector }, + ...restParsedCreateCase + } = parsedCaseNewValue; + + const { + connector: { id: ignoreConnectorId, ...restConnector }, + ...restCreateCase + } = caseRequest; + + expect(restParsedCreateCase).to.eql({ ...restCreateCase, type: CaseType.individual }); + expect(restParsedConnector).to.eql(restConnector); + + expect(objects[1].attributes.old_value).to.eql(null); + expect(includesAllRequiredFields(objects[1].attributes.action_field)).to.eql(true); + + // should be the comment + expect(objects[2].attributes.comment).to.eql(postCommentUserReq.comment); + expect(objects[2].attributes.type).to.eql(postCommentUserReq.type); + + expect(objects[3].attributes.action).to.eql('create'); + expect(JSON.parse(objects[3].attributes.new_value)).to.eql(postCommentUserReq); + expect(objects[3].attributes.old_value).to.eql(null); + expect(objects[3].attributes.action_field).to.eql(['comment']); + }); + + it('imports a case with a comment and user actions', async () => { + await supertest + .post('/api/saved_objects/_import') + .query({ overwrite: true }) + .attach( + 'file', + join( + __dirname, + '../../../../common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson' + ) + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const findResponse = await findCases({ supertest, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case to export'); + expect(findResponse.cases[0].description).to.eql('a description'); + + const { body: commentsResponse }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) + .send() + .expect(200); + + const comment = (commentsResponse.comments[0] as unknown) as AttributesTypeUser; + expect(comment.comment).to.eql('A comment for my case'); + + const userActions = await getCaseUserActions({ + supertest, + caseID: findResponse.cases[0].id, + }); + + expect(userActions).to.have.length(2); + expect(userActions[0].action).to.eql('create'); + expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); + + expect(userActions[1].action).to.eql('create'); + expect(userActions[1].action_field).to.eql(['comment']); + expect(userActions[1].old_value).to.eql(null); + expect(JSON.parse(userActions[1].new_value!)).to.eql({ + comment: 'A comment for my case', + type: 'user', + owner: 'securitySolution', + }); + }); + }); +}; + +const ndjsonToObject = (input: string) => { + return input.split('\n').map((str) => JSON.parse(str)); +}; + +const includesAllRequiredFields = (actionFields: string[]): boolean => { + const requiredFields = [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ]; + + return requiredFields.every((field) => actionFields.includes(field)); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index 9b24de26245f42..8f4e0d1ab17943 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -20,6 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./alerts/get_alerts_attached_to_case')); loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/export')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); From 5f8895e04bb2b64b3486d3b2ac597245b31c8a6f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 31 Aug 2021 13:59:12 -0400 Subject: [PATCH 4/7] Adding connector test --- ...ingle_case_user_actions_one_comment.ndjson | 0 ..._case_with_connector_update_to_none.ndjson | 0 .../tests/common/cases/export.ts | 48 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) rename x-pack/test/case_api_integration/common/fixtures/{case_exports => saved_object_exports}/single_case_user_actions_one_comment.ndjson (100%) rename x-pack/test/case_api_integration/common/fixtures/{case_exports => saved_object_exports}/single_case_with_connector_update_to_none.ndjson (100%) diff --git a/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson similarity index 100% rename from x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson rename to x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson diff --git a/x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson similarity index 100% rename from x-pack/test/case_api_integration/common/fixtures/case_exports/single_case_with_connector_update_to_none.ndjson rename to x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts index 986204ad0a4c64..b902f8765fa114 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts @@ -101,7 +101,53 @@ export default ({ getService }: FtrProviderContext): void => { 'file', join( __dirname, - '../../../../common/fixtures/case_exports/single_case_user_actions_one_comment.ndjson' + '../../../../common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson' + ) + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const findResponse = await findCases({ supertest, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case to export'); + expect(findResponse.cases[0].description).to.eql('a description'); + + const { body: commentsResponse }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) + .send() + .expect(200); + + const comment = (commentsResponse.comments[0] as unknown) as AttributesTypeUser; + expect(comment.comment).to.eql('A comment for my case'); + + const userActions = await getCaseUserActions({ + supertest, + caseID: findResponse.cases[0].id, + }); + + expect(userActions).to.have.length(2); + expect(userActions[0].action).to.eql('create'); + expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); + + expect(userActions[1].action).to.eql('create'); + expect(userActions[1].action_field).to.eql(['comment']); + expect(userActions[1].old_value).to.eql(null); + expect(JSON.parse(userActions[1].new_value!)).to.eql({ + comment: 'A comment for my case', + type: 'user', + owner: 'securitySolution', + }); + }); + + it('imports a case with a connector', async () => { + await supertest + .post('/api/saved_objects/_import') + .query({ overwrite: true }) + .attach( + 'file', + join( + __dirname, + '../../../../common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson' ) ) .set('kbn-xsrf', 'true') From 020698648f93de734a2c333c6d4b66bc4d5a2c16 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 1 Sep 2021 10:46:24 -0400 Subject: [PATCH 5/7] Finishing integration tests --- .../cases/{export.ts => import_export.ts} | 35 +++++++++---------- .../security_and_spaces/tests/common/index.ts | 2 +- 2 files changed, 17 insertions(+), 20 deletions(-) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/{export.ts => import_export.ts} (88%) diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts similarity index 88% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts index b902f8765fa114..78eeb8064a0e99 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/export.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -28,7 +28,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('export cases', () => { + describe('import and export cases', () => { afterEach(async () => { await deleteAllCaseItems(es); }); @@ -155,34 +155,31 @@ export default ({ getService }: FtrProviderContext): void => { const findResponse = await findCases({ supertest, query: {} }); expect(findResponse.total).to.eql(1); - expect(findResponse.cases[0].title).to.eql('A case to export'); - expect(findResponse.cases[0].description).to.eql('a description'); - - const { body: commentsResponse }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) - .send() - .expect(200); - - const comment = (commentsResponse.comments[0] as unknown) as AttributesTypeUser; - expect(comment.comment).to.eql('A comment for my case'); + expect(findResponse.cases[0].title).to.eql('A case with a connector'); + expect(findResponse.cases[0].description).to.eql('super description'); const userActions = await getCaseUserActions({ supertest, caseID: findResponse.cases[0].id, }); - expect(userActions).to.have.length(2); + expect(userActions).to.have.length(3); expect(userActions[0].action).to.eql('create'); expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); - expect(userActions[1].action).to.eql('create'); - expect(userActions[1].action_field).to.eql(['comment']); + expect(userActions[1].action).to.eql('push-to-service'); + expect(userActions[1].action_field).to.eql(['pushed']); expect(userActions[1].old_value).to.eql(null); - expect(JSON.parse(userActions[1].new_value!)).to.eql({ - comment: 'A comment for my case', - type: 'user', - owner: 'securitySolution', - }); + + const parsedPushNewValue = JSON.parse(userActions[1].new_value!); + expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); + expect(parsedPushNewValue.connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); + + expect(userActions[2].action).to.eql('update'); + expect(userActions[2].action_field).to.eql(['connector']); + + const parsedUpdateNewValue = JSON.parse(userActions[2].new_value!); + expect(parsedUpdateNewValue.id).to.eql('none'); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index 8f4e0d1ab17943..fba60634cc3d7b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -20,7 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./alerts/get_alerts_attached_to_case')); loadTestFile(require.resolve('./cases/delete_cases')); - loadTestFile(require.resolve('./cases/export')); + loadTestFile(require.resolve('./cases/import_export')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); From 93c25fdec6e924134fd2df415a010ea2dd1ef3cc Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 1 Sep 2021 13:43:13 -0400 Subject: [PATCH 6/7] Removing connector after each test to restore original state --- .../security_and_spaces/tests/common/cases/import_export.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts index 78eeb8064a0e99..df4e858e8a2907 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { join } from 'path'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { deleteAllCaseItems, createCase, @@ -29,8 +30,11 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('import and export cases', () => { + const actionsRemover = new ActionsRemover(supertest); + afterEach(async () => { await deleteAllCaseItems(es); + await actionsRemover.removeAll(); }); it('exports a case with its associated user actions and comments', async () => { @@ -153,6 +157,8 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); + actionsRemover.add('default', '1cd34740-06ad-11ec-babc-0b08808e8e01', 'action', 'actions'); + const findResponse = await findCases({ supertest, query: {} }); expect(findResponse.total).to.eql(1); expect(findResponse.cases[0].title).to.eql('A case with a connector'); From 9aee1941e00e3f03b860561f3a399d0692e14d11 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 7 Sep 2021 13:04:05 -0400 Subject: [PATCH 7/7] Bumping migration version for comment so --- .../single_case_user_actions_one_comment.ndjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson index 79442d5feeb77e..2fb02f297c6eae 100644 --- a/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson +++ b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson @@ -1,5 +1,5 @@ {"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T19:48:01.292Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"a description","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual","updated_at":"2021-08-26T19:48:30.151Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases":"7.15.0"},"references":[],"type":"cases","updated_at":"2021-08-26T19:48:30.162Z","version":"WzM0NDEsMV0="} {"attributes":{"action":"create","action_at":"2021-08-26T19:48:01.292Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case to export\",\"tags\":[\"some tags\"],\"description\":\"a description\",\"connector\":{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"8cb85070-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007281292,7288],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:13.687Z","version":"WzIzODIsMV0="} -{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-08-26T19:48:30.151Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.0.0","id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-comments":"7.15.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007310151,9470],"type":"cases-comments","updated_at":"2021-08-26T19:48:30.161Z","version":"WzM0NDIsMV0="} +{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-08-26T19:48:30.151Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.0.0","id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-comments":"7.16.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007310151,9470],"type":"cases-comments","updated_at":"2021-08-26T19:48:30.161Z","version":"WzM0NDIsMV0="} {"attributes":{"action":"create","action_at":"2021-08-26T19:48:30.151Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["comment"],"new_value":"{\"comment\":\"A comment for my case\",\"type\":\"user\",\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"9710c840-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"},{"id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1630007310151,9542],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:31.044Z","version":"WzM1MTIsMV0="} {"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]}